feat:mix:add examples/

This commit is contained in:
liujian 2021-08-03 16:35:53 +08:00
parent b780c3db7b
commit f31bdadb1b
107 changed files with 4048 additions and 0 deletions

View 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
View 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

View 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/

View 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;
}

View 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();

View 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();
});

View 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();

View 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"
}
}

View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

View 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();

View 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');
};

View File

@ -0,0 +1,2 @@
*
!.gitignore

View 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

View 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';
}
}

View 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;
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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));
}
}

View 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.
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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)));
}
}

View 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,
]
]);
}
}

View 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!');
}
}

View 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
]);
}
}

View 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);
}
}

View 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();
};
}
}

View 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();
};
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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

View 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/

View 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;
}

View 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();

View 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();
});

View 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"
}
}

View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

View 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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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());
}
}

View File

@ -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;
}

View 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;
}

View File

@ -0,0 +1,2 @@
*
!.gitignore

View 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

View 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';
}
}

View 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;
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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));
}
}

View 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.
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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)));
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
View 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

View 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/

View 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;
}

View 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();

View 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();
});

View 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();

View 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"
}
}

View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View 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();

View File

@ -0,0 +1,7 @@
<?php
use App\Controller\Hello;
return function (Mix\Vega\Engine $vega) {
$vega->handle('/', [new Hello(), 'index'])->methods('GET');
};

View File

@ -0,0 +1,2 @@
*
!.gitignore

View 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

View 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';
}
}

View 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;
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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));
}
}

View 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.
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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)));
}
}

View 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!'
]);
}
}

View 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);
}
}

View 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;
}
}

View 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;
}
}

View File

@ -0,0 +1,5 @@
<html>
<h1>
<?= $title ?>
</h1>
</html>

View 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
View 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

View 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/

View 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;
}

View 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();
});

View 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"
}
}

View File

@ -0,0 +1,3 @@
{
"foo": "bar"
}

View File

@ -0,0 +1,7 @@
<?php
use App\Controller\WebSocket;
return function (Mix\Vega\Engine $vega) {
$vega->handle('/websocket', [new WebSocket(), 'index'])->methods('GET');
};

View File

@ -0,0 +1,2 @@
*
!.gitignore

View 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

View 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';
}
}

View 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;
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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));
}
}

View 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.
}
}

View 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(); // 必须放到最后,防止触发协程调度导致异常
}
}

View 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)));
}
}

View 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