前言
本项目基于workerman实现支持websocket、tcp协议的PHP框架,mvc模式,跟传统http框架运行原理相差不多,只是入口依赖于workerman接收到的消息 项目源码可以移步github参考,欢迎star https://github.com/feixuekeji/flysocket
入口
入口文件是GatewayWorker下Event.php 入口处需要引入下自动加载函数,以便框架里函数调用 require_once DIR . ‘/…/lib/Autoloader.php’; 主要有两个函数 onWorkerStart 项目启动时初始化框架 onMessage 接受到客户端信息,收到消息转发给框架执行请求,相当于收到http请求信息
require_once __DIR__ . '/../lib/Autoloader.php';
public static function onWorkerStart($worker)
{
Container::get('app')->init($worker->id);
}
public static function onMessage($client_id, $message)
{
if ($message == 'ping')
return;
App::run($client_id, $message);
}
app
app主要包含框架初始化跟执行两个功能 初始化init函数主要功能包括 路由加载,Session,数据库等初始化
运行函数run 负责收到请求后具体执行, 解析请求数据 组装request请求对象 路由分发执行 将执行结果返回客户端
App.php
public function __construct()
{
$this->rootPath = __DIR__ . DIRECTORY_SEPARATOR . '../';
$this->routePath = $this->rootPath . 'route' . DIRECTORY_SEPARATOR;
$this->configPath = $this->rootPath . 'config' . DIRECTORY_SEPARATOR;
$this->runtimePath = $this->rootPath . 'runtime' . DIRECTORY_SEPARATOR;
}
public function run($client_id, $message)
{
try {
$message = json_decode($message,true);
!is_array($message) && $message = [];
$request = Container::get('request',[$message]);
$res = Route::dispatch($request);
$response = $request->response($res['data'],$res['code'],$res['msg']);
} catch (\Throwable $e) {
Error::exception($e);
$response = $request->response('',$e->getCode() ?: 1,$e->getMessage());
}
Gateway::sendToClient($client_id, json_encode($response));
Container::remove('request');
}
public function init($workerId)
{
try {
$log = Container::get('log',[Config::get('','log')]);
$cache = Container::get('cache',[Config::get('','cache')]);
Container::get('session',[Config::get('','session')]);
Container::get('route')->import();
Db::setConfig(Config::get('','database'));
Db::setCache($cache);
Db::setLog($log);
$workerId == 0 && $this->corn();
} catch (\Exception $e) {
Error::exception($e);
}
}
自动加载
spl_autoload_register(‘\lib\Autoloader::load’); 注册自动加载函数,程序执行时如果该文件没有加载会调用自动加载函数进行文件引入。
采用命名空间方式定义和自动加载类库文件,遵循psr-0规范,只需要给类库正确定义所在的命名空间,并且命名空间的路径与类库文件的目录一致,那么就可以实现类的自动加载 命名空间跟文件路径一致,只需将类名中的斜杠转成反斜杠就是文件所在路径,如果有更多规则,可具体判断加载
如Admin.php的命名空间为 namespace application\admin\controller;则加载的文件路径为application/admin/controller/Admin.php
Autoloader.php
namespace lib;
class Autoloader
{
public static function load($className)
{
$classPath = str_replace('\\', '/', $className);
$classFile = __DIR__ .'/../'.$classPath.'.php';
if (is_file($classFile)) {
require_once($classFile);
if (class_exists($className, false)) {
return true;
}
}
return false;
}
}
spl_autoload_register('\lib\Autoloader::load');
路由分发
dispatch函数 根据request请求里的模块、控制器、方法名,拼装出需要调用的类名, 调用目标类 然后使用call_user_func_array调用类中方法执行
import 读取路由配置文件,加载路由表,Request里获取模块名,控制器,方法时用到
Route.php
class Route
{
protected $routeList = [];
public static function dispatch(Request $request)
{
$module = $request->module();
$controller = $request->controller();
$action = $request->action();
if (!$module || !$controller || !$action)
throw new \Exception('api is not exists',100);
$className = '\\application\\' . $module . '\\controller\\' . ucfirst($controller);
$obj = new $className($request);
if (!method_exists($obj, $action))
throw new \Exception('method ' . $action . ' is not exists',100);
$res = call_user_func_array(array($obj, $action), array($request));
return $res;
}
public function import()
{
$path = Container::get('app')->getRoutePath();
$files = is_dir($path) ? scandir($path) : [];
foreach ($files as $file) {
if (strpos($file, '.php')) {
$filename = $path . DIRECTORY_SEPARATOR . $file;
$rules = include $filename;
if (is_array($rules)) {
$this->routeList = array_merge($this->routeList,$rules);
}
}
}
}
public function getRoute($api)
{
if (array_key_exists($api, $this->routeList))
$api = $this->routeList[$api];
return $api;
}
}
请求
主要功能包括请求信息(如IP,端口,参数)的初始化以及参数的获取安全过滤,以供在控制起来使用 本项目里自定义请求信息如下,以json文本形式传输
$param = [
'api' => 'my-info',
'app' => 'iphone',
'ver' => '1.0',
'data' => [
],
];
Request.php
private static $_instance;
public function __construct(array $options = [])
{
$this->init($options);
self::$_instance = $this;
}
public function init(array $options = [])
{
$this->filter = Config::get('default_filter');
$this->param = $options['data'] ?? [];
$this->api = $options['api'] ?? '';
$this->route = $options['api'] ?? '';
$this->app = $options['app'] ?? '';
$this->ver = $options['ver'] ?? '';
$this->route = Container::get('route')->getRoute($this->route);
$api = explode('/',$this->route);
$this->setModule($api[0] ?? '');
$this->setController($api[1] ?? '');
$this->setAction($api[2] ?? '');
$this->ip = $this->ip();
$this->setPort();
$this->setGatewayIp();
$this->setGatewayPort();
$this->setClientId();
\lib\facade\Log::info('request',get_object_vars($this));
}
配置文件
用于从文件里获取配置信息
Config.php
class Config
{
protected static $config;
static function load($file){
if (!isset(self::$config[$file])){
$confFile = __DIR__ . '/../config/' . $file .'.php';
if (is_file($confFile)){
self::$config[$file] = include_once $confFile;
}
}
}
public static function get($name = null,$file = 'config', $default = null)
{
self::load($file);
if (empty($name)) {
return self::$config[$file];
}
$name = explode('.', $name);
$name[0] = strtolower($name[0]);
$config = self::$config[$file];
foreach ($name as $val) {
if (isset($config[$val])) {
$config = $config[$val];
} else {
return $default;
}
}
return $config;
}
redis
Redis.php
class Redis
{
private static $_instance;
private function __construct( ){
$config = Config::get('redis');
self::$_instance = new \Redis();
self::$_instance->connect($config['host'], $config['port']);
self::$_instance->select($config['db_index']);
if ('' != $config['password']) {
self::$_instance->auth($config['password']);
}
}
public static function getInstance( )
{
if (!self::$_instance) {
new self();
}
else{
try {
@trigger_error('flag', E_USER_NOTICE);
self::$_instance->ping();
$error = error_get_last();
if($error['message'] != 'flag')
throw new \Exception('Redis server went away');
} catch (\Exception $e) {
new self();
}
}
return self::$_instance;
}
private function __clone(){}
public function __call($method,$args)
{
return call_user_func_array([self::$_instance, $method], $args);
}
public static function __callStatic($method,$args)
{
self::getInstance();
return call_user_func_array([self::$_instance, $method], $args);
}
缓存
缓存遵循psr-16规范,本项目里只支持Redis的实现
Cache.php
class Cache implements Psr16CacheInterface
{
protected $handler = null;
protected $options = [
'expire' => 0,
'prefix' => '',
'serialize' => true,
];
public function __construct($options = []){
if (!empty($options)) {
$this->options = array_merge($this->options, $options);
}
}
public function get($name, $default = null)
{
$this->handler = Redis::getInstance();
$key = $this->getCacheKey($name);
$value = $this->handler->get($key);
if (is_null($value) || false === $value) {
return $default;
}
return $this->unserialize($value);
}
public function set($name, $value, $expire = null)
{
$this->handler = Redis::getInstance();
if (is_null($expire)) {
$expire = $this->options['expire'];
}
$value = $this->serialize($value);
$key = $this->getCacheKey($name);
if ($expire) {
$result = $this->handler->setex($key, $expire, $value);
} else {
$result = $this->handler->set($key, $value);
}
return $result;
}
public function getMultiple($keys, $default = null)
{
if (!\is_array($keys)) {
throw new \Exception(sprintf('Cache keys must be array or Traversable, "%s" given', \is_object($keys) ? \get_class($keys) : \gettype($keys)));
}
$result = [];
foreach ($keys as $key) {
$result[$key] = $this->get($key);
}
return $result;
}
public function setMultiple($values, $expire = null)
{
if (!\is_array($values)) {
throw new \Exception(sprintf('Cache values must be array or Traversable, "%s" given', \is_object($values) ? \get_class($values) : \gettype($values)));
}
if (is_null($expire)) {
$expire = $this->options['expire'];
}
try {
foreach ($values as $key => $value) {
if (\is_int($key)) {
$key = (string) $key;
}
$this->set($key,$value,$expire);
}
return true;
} catch (\Exception $e) {
return false;
}
}
public function delete($name)
{
$this->handler = Redis::getInstance();
$key = $this->getCacheKey($name);
try {
$this->handler->del($key);
return true;
} catch (\Exception $e) {
return false;
}
}
public function deleteMultiple($keys)
{
$this->handler = Redis::getInstance();
if (!\is_array($keys)) {
throw new Exception(sprintf('Cache keys must be array or Traversable, "%s" given', \is_object($keys) ? \get_class($keys) : \gettype($keys)));
}
foreach ($keys as &$item){
$item = $this->getCacheKey($item);
}
try {
$this->handler->del($keys);
return true;
} catch (\Exception $e) {
return false;
}
}
public function clear()
{
$this->handler = Redis::getInstance();
return $this->handler->flushDB();
}
public function has($name)
{
$this->handler = Redis::getInstance();
$key = $this->getCacheKey($name);
return $this->handler->exists($key);
}
protected function serialize($data)
{
if (is_scalar($data) || !$this->options['serialize']) {
return $data;
}
$data = 'serialize:'.serialize($data);
return $data;
}
protected function unserialize($data)
{
if ($this->options['serialize'] && 0 === strpos($data, 'serialize:')) {
return unserialize(substr($data, 10));
} else {
return $data;
}
}
protected function getCacheKey($name)
{
return $this->options['prefix'] . $name;
}
数据库ORM
ORM自己造轮子有点复杂就使用了ThinkOrm,大多数方法跟tp一致
日志
日志日志遵循PSR-3规范基于Monolog实现
Log.php
class Log implements LoggerInterface
{
protected $loggers;
protected $allowWrite = true;
protected $config = [
'path' => '',
'channel' => 'app',
'level' => 'debug',
'max_files' => 0,
'file_permission' => 0666,
'sql_level' => 'info',
];
public function __construct($config = [])
{
if (is_array($config)) {
$this->config = array_merge($this->config, $config);
}
if (!empty($config['close'])) {
$this->allowWrite = false;
}
if (empty($this->config['path'])) {
$this->config['path'] = \lib\facade\App::getRootPath() . 'runtime/logs' . DIRECTORY_SEPARATOR;
} elseif (substr($this->config['path'], -1) != DIRECTORY_SEPARATOR) {
$this->config['path'] .= DIRECTORY_SEPARATOR;
}
}
private function createLogger($name,$fileName = '')
{
if (empty($this->loggers[$name.$fileName])) {
$channel = $this->config['channel'];
$path = $this->config['path'];
$maxFiles = $this->config['max_files'];
$level = Logger::toMonologLevel($this->config['level']);
$filePermission = $this->config['file_permission'];
$logger = new Logger($channel);
$logFileName = empty($fileName) ? $name : $name . '-' .$fileName;
$handler = new RotatingFileHandler("{$path}{$logFileName}.log", $maxFiles, $level, true, $filePermission);
$formatter = new LineFormatter("%datetime% %channel%:%level_name% %message% %context% %extra%\n", "Y-m-d H:i:s", false, true);
$handler->setFormatter($formatter);
$logger->pushHandler($handler);
$this->loggers[$name.$fileName] = $logger;
}
return $this->loggers[$name.$fileName];
}
public function record($message, $level = 'info', array $context = [],$fileName = '')
{
if (!$this->allowWrite) {
return;
}
$logger = $this->createLogger($level,$fileName);
$level = Logger::toMonologLevel($level);
if (!is_int($level)) $level = Logger::INFO;
if (version_compare(PCRE_VERSION, '7.0.0', '>=')) {
$backtrace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
$idx = 0;
} else {
$backtrace = debug_backtrace();
$idx = 1;
}
$trace = basename($backtrace[$idx]['file']) . ":" . $backtrace[$idx]['line'];
if (!empty($backtrace[$idx + 1]['function'])) {
$trace .= '##';
$trace .= $backtrace[$idx + 1]['function'];
}
$message = sprintf('==> LOG: %s -- %s', $message, $trace);
return $logger->addRecord($level, $message, $context);
}
public function log($level, $message, array $context = [], $fileName = '')
{
if ($level == 'sql')
$level = $this->config['sql_level'];
$this->record($message, $level, $context, $fileName);
}
public function emergency($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function alert($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function critical($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function error($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function warning($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function notice($message, array $context = [],$fileName = '')
{
$this->log(__FUNCTION__, $message, $context, $fileName);
}
public function info($message, array $context = [],$fileName = '')
{
$this->log(__FUNCTION__, $message, $context, $fileName);
}
public function debug($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
public function sql($message, array $context = [])
{
$this->log(__FUNCTION__, $message, $context);
}
容器
session
主要包括session的读写功能
Session.php
public function __construct(array $config = [])
{
$this->config = $config;
}
public function prefix($prefix = '')
{
if (empty($prefix) && null !== $prefix) {
return $this->prefix;
} else {
$this->prefix = $prefix;
}
}
public static function __make(Config $config)
{
return new static($config->get('','session'));
}
public function setConfig(array $config = [])
{
$this->config = array_merge($this->config, array_change_key_case($config));
if (isset($config['prefix'])) {
$this->prefix = $config['prefix'];
}
}
public function init(array $config = [])
{
$config = $config ?: $this->config;
if (isset($config['prefix'])) {
$this->prefix = $config['prefix'];
}
$this->init = true;
return $this;
}
public function set($name, $value, $prefix = null)
{
is_null($this->init) && $this->init();
$prefix = !is_null($prefix) ? $prefix : $this->prefix;
if (strpos($name, '.')) {
list($name1, $name2) = explode('.', $name);
if ($prefix) {
$_SESSION[$prefix][$name1][$name2] = $value;
} else {
$_SESSION[$name1][$name2] = $value;
}
} elseif ($prefix) {
$_SESSION[$prefix][$name] = $value;
} else {
$_SESSION[$name] = $value;
}
}
public function get($name = '', $prefix = null)
{
is_null($this->init) && $this->init();
$prefix = !is_null($prefix) ? $prefix : $this->prefix;
$value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION;
if ('' != $name) {
$name = explode('.', $name);
foreach ($name as $val) {
if (isset($value[$val])) {
$value = $value[$val];
} else {
$value = null;
break;
}
}
}
return $value;
}
public function pull($name, $prefix = null)
{
$result = $this->get($name, $prefix);
if ($result) {
$this->delete($name, $prefix);
return $result;
} else {
return;
}
}
public function delete($name, $prefix = null)
{
is_null($this->init) && $this->init();
$prefix = !is_null($prefix) ? $prefix : $this->prefix;
if (is_array($name)) {
foreach ($name as $key) {
$this->delete($key, $prefix);
}
} elseif (strpos($name, '.')) {
list($name1, $name2) = explode('.', $name);
if ($prefix) {
unset($_SESSION[$prefix][$name1][$name2]);
} else {
unset($_SESSION[$name1][$name2]);
}
} else {
if ($prefix) {
unset($_SESSION[$prefix][$name]);
} else {
unset($_SESSION[$name]);
}
}
}
public function clear($prefix = null)
{
is_null($this->init) && $this->init();
$prefix = !is_null($prefix) ? $prefix : $this->prefix;
if ($prefix) {
unset($_SESSION[$prefix]);
} else {
$_SESSION = [];
}
}
public function has($name, $prefix = null)
{
is_null($this->init) && $this->init();
$prefix = !is_null($prefix) ? $prefix : $this->prefix;
$value = $prefix ? (!empty($_SESSION[$prefix]) ? $_SESSION[$prefix] : []) : $_SESSION;
$name = explode('.', $name);
foreach ($name as $val) {
if (!isset($value[$val])) {
return false;
} else {
$value = $value[$val];
}
}
return true;
}
public function push($key, $value)
{
$array = $this->get($key);
if (is_null($array)) {
$array = [];
}
$array[] = $value;
$this->set($key, $array);
}
异常
用来注册异常处理函数,捕捉全局异常并记录错误日志 Error.php
class Error
{
protected static $exceptionHandler;
public static function register()
{
error_reporting(E_ALL);
set_error_handler([__CLASS__, 'error']);
set_exception_handler([__CLASS__, 'exception']);
register_shutdown_function([__CLASS__, 'shutdown']);
}
public static function exception($e)
{
self::report($e);
}
public static function report(Throwable $exception)
{
$data = [
'file' => $exception->getFile(),
'line' => $exception->getLine(),
'message' => $exception->getMessage(),
'code' => $exception->getCode(),
];
\lib\facade\Log::error('错误信息',$data);
\lib\facade\Log::error('错误跟踪',$exception->getTrace());
}
public static function error($errno, $errstr, $errfile = '', $errline = 0): void
{
$data = [
'file' => $errfile,
'line' =>$errline,
'message' => $errstr,
'code' => $errno,
];
\lib\facade\Log::error('错误信息',$data);
}
public static function errorLog(\Error $error): void
{
$data = [
'file' => $error->getFile(),
'line' => $error->getLine(),
'message' => $error->getMessage(),
'code' => $error->getCode(),
];
\lib\facade\Log::error('错误信息',$data);
}
public static function shutdown()
{
if (!is_null($error = error_get_last()) && self::isFatal($error['type'])) {
self::error($error);
}
}
protected static function isFatal($type)
{
return in_array($type, [E_ERROR, E_CORE_ERROR, E_COMPILE_ERROR, E_PARSE]);
}
控制器
控制器基类 初始化时依赖注入Request对象,继承该类的控制器可直接使用。
Controlle.php
class Controller
{
protected $request;
public function __construct(Request $request)
{
$this->request = Container::get('request');
$this->initialize();
}
protected function initialize()
{}
}
使用方法
移步 https://www.kancloud.cn/xiongfeifei/ver1/2131252
|