十多个核心文件,百行代码,就可以手写实现极简php轻量级框架,并拥有常用功能。项目初衷是秉承大道至简理念,能够在本地机器环境下,快速搭建项目脚手架,实现常用脚本功能实现和API demo等,不需要太复杂和花哨的功能和复杂的重型框架学习与安装依赖,主打一个极简和快速上手。
通过以下文档,可以快速理解和掌握项目的基本实现原理和部署,项目抛砖引入,项目特点如下:
- 支持
PSR代码规范 - 支持
MySQL,MongoDB等常用数据库 - 支持
Redis缓存操作 - 支持
web服务器(基于Swoole和原生模式),实现简单MVC - 支持
cli运行时下console脚本模式 - 支持第三方扩展
- 支持
docker环境和本地环境简单部署
App |----Console !----|----Base.php |----|----Advt |----|----Mysql |----|----Mongo |----|----Redis ... |----Controller !----|----Base.php |----|----Index |----|----User ... |----Response |----Constant |----Func |----Lib |----Logic |----|----Advt ... |----Model !----AbstractModel.php |----|----User ... |----Service |----|----Mongo |----|----|----BaseService.php ... |----|----User ... |----Task |----|----BaseTask.php ... config statics composer.json console.php index.php
项目遵循主流框架常用代码结构,结构清晰明了,易于扩展,包括入口文件,配置文件,静态文件、项目依赖文件、App文件等核心文件,下面简要介绍核心文件功能:
项目根目录文件负责项目入口功能,包含了基础配置,依赖管理,程序入口等功能分区等,功能固定,不需要扩展
-
index.php项目 入口文件之一,内置基于Swoole扩展的http服务器,负责参数解析、mvc路由解析、应用实例创建和请求响应输出等核心功能 -
console.php项目入口文件之一,原生cli模式实现命令行脚本,支持固定路由模式和基本参数解析,简单实现业务脚本处理 -
composer.json项目依赖注册文件,基于composer方便快速安装扩展第三方包应用 -
statics静态目录,方便自动任务或者web控制器处理文件的工具目录 -
config配置文件,支持默认配置+环境变量配置模式,数组化节点配置,简单好理解
应用目录文件也就是App文件夹下的各个子文件组合,承接着框架控制器应用和自动任务应用,以及各种公共类库服务,全局功能定义,支持按业务区分扩展等,以下安装功能模块进行讲解:
Controller 控制器目录,该目录构成为:Base.php 加上多个 模块控制器的的组合,其中Base.php 为控制器父类的实现,具体业务控制器都要继承该类,如Controller/Index/IndexController.php 继承自父类控制器,Index是默认的控制器模块,IndexController是默认控制器;模块命名规范为首字母大写,模块控制器命名规范为驼峰法控制器名+Controller.php ,控制器类名必须和文件名保持一致。
如果想扩展一个Student模块控制器,可以新建Controller/Student/IndexController.php 文件,具体写法可参考Index模块控制器即可,代码示例如下:
<?php namespace App\Controller\Student; use Context; use App\Controller\Base; use App\Response\Response; class IndexController extends Base{ public function IndexAction(){ $sid = $this->params['id']?:1; return Context::get(Response::class)->success( 'hello,student', [ 'sid' => $sid ] ); } }
Console 自动任务目录,该目录构成和Controller类似:Base.php 加上多个 模块的组合,其中Base.php 为自动任务父类的实现,具体业务自动任务都要继承该类,如Console/Index/IndexConsole.php Index是默认的自动任务模块,IndexConsole.php是模块默认自动任务文件,模块命名规范上模块名首字母大写,自动任务命名比较灵活,首字母大写和驼峰发组合,类命和文件名保持一致;如Console/Mysql/TestMysql.php 表示Mysql模块下的TestMysql自动任务,示例代码如下:
<?php namespace App\Console\MySql; use App\Console\Base; use App\Func\Config; use Illuminate\Database\Capsule\Manager as DB; class TestMysql extends Base{ public function run(){ $db = Config::get('db'); var_dump($db); $group = DB::table('group')->where('id','>',1)->get(); var_dump($group); } }
公共模块是框架中的核心类库文件,如常量定义、核心函数库、响应类、助手类、模型、服务等,这些文件均可以被Controller和Console复用,因此被视为基础公共文件部分,下面分别介绍各个文件模块主要功能。
Constant 目录 系统常量定义
Func 目录:常用工具类文件,使用场景,可在应用(Controller/Console)启动注入初始化,在Model/Service/Logic当中使用,可根据业务需求进行扩展
Lib 目录:常用类库文件,使用场景,可在应用(Controller/Console)启动注入初始化,在Model/Service/Logic当中使用可根据业务需求扩展
Logic 目录:业务逻辑封装,可在应用(Controller/Console)中直接调用,根据实际需求按功能模块目录区分
Model 目录:DB封装,这里使用了Laravel框架的Eloquent ORM 组件,按照Laravel方式进行数据模型封装即可
Task目录: 简单的MongoDB集合定义,这里定义了Mongo collection信息
Response 目录:自定义了响应数据封装,使用场景,在应用启动注册,在控制器输出中使用
Service 目录: 服务封装,注意这里将MongoDB相关操作进行了单独封装,其他模块服务也可以放在该目录下,常用使用场景,对一些主要数据库操作进行封装,方便其他程序模块调用
这里对一些核心文件进行解析,方便了解整个开发逻辑,结合具体示例,介绍一个完整控制器或自动任务调用链条,以加强对框架的理解,具体会以两条线:控制器和自动任务进行代码分析。
以console.php文件为例说明:
<?php define("PROJECT_PATH",str_replace(str_replace("\\","/",__NAMESPACE__),"",__DIR__)); define("CONSOLE_PATH",PROJECT_PATH.DIRECTORY_SEPARATOR."App".DIRECTORY_SEPARATOR."Console".DIRECTORY_SEPARATOR); /** * @usage php console.php r=Mysql/TestMysql */ class Application { public $route; public $argv; public $defaultns = "App\\Console\\"; public function __construct($argv){ $this->argv = $argv; } public function getParams() { $params = []; array_shift($this->argv); foreach ($this->argv as $k => $v) { $tmpArr = explode("=", $v); $params[reset($tmpArr)] = end($tmpArr); } return $params; } public function parseRoute() { $this->route = explode("/",$this->getParams()['r']??''); if (empty($this->route)) exit("router r is required ! r=xx"); array_walk($this->route ,function(&$itm) { $itm = ucfirst($itm); }); } public static function useImplode($separator,$arr) { $str = ""; foreach($arr as $k => $v) { $str.= sprintf("%s%s",$v, $k==count($arr)-1 ? "" : $separator); } return $str; } //auto_laod class public function loadClass() { require_once CONSOLE_PATH."Base.php"; require_once CONSOLE_PATH. self::useImplode(DIRECTORY_SEPARATOR,$this->route).".php"; } public function getInstance(){ $className = sprintf("%s%s",$this->defaultns,self::useImplode("\\",$this->route)); return new $className($this->argv); } public function start() { $this->parseRoute(); $this->loadClass(); return $this->getInstance()->run(); } } $app = new Application($argv); $app->start();
执行一个Mysql模块下的TestMysql.php脚本,命令如下:
php console.php r=Mysql/TestMysql
整个自动任务执行流程很简单,Application类接收$argv变量后进行实例化对象,实例对象执行start()方法,完成自动任务脚本执行,再看整个类都是基于start()方法定义函数功能。
cli下执行php时,使用$argv可以获取shell脚本全部参数列表,通过类的构造函数注入$argv变量赋值到类的属性argv上,再通过getParams()方法格式化参数,获取到有用的参数数组,方便后续路由数据解析。
parseRoute()方法实现具体路由解析,通过前面获取到传递的参数数组,可以拆分出自动任务信息,即通过r参数结果得到自动任务模块和脚本名,并格式化赋值到类的route属性上,方便后续使用。
完成路由变量解析后,开始执行loadClass()方法,核心逻辑就是引入自动任务目录下的父类App/Console/Base.php和继承父类的模块脚本,也就是解析路由结果中的文件信息,通过loadClass()完成了相关依赖文件的加载
getInstance()->run()方法,对route信息中的自动任务脚本类进行实例化并执行该类继续父类的抽象方法,得到最后的输出结果,整个自动任务执行完毕
在整个流程中,App/Console/Base.php中作为自动任务类的父类,地位相当重要,让我看下这个类代码实现:
<?php namespace App\Console; //设置全局常量 define("ROOT_PATH",str_replace(str_replace("\\","/",__NAMESPACE__),"",__DIR__)); define("BASE_PATH",ROOT_PATH.DIRECTORY_SEPARATOR."App".DIRECTORY_SEPARATOR."Console".DIRECTORY_SEPARATOR); define("STATIC_PATH",ROOT_PATH."statics"); define('APPLICATION_ENV', $_ENV['SERVER_ENV'] ?? 'test'); use App\Func\Config; use App\Func\MongoTool; use App\Func\RedisTool; use Illuminate\Database\Capsule\Manager as DB; abstract class Base { public $params; public $basePath; abstract public function run(); public function __construct($argv) { $this->params = $this->getParams($argv); $this->basePath = $this->getBasePath(); $this->bootstrap(); } public function bootstrap() { $this->autoLoadClass(); $this->loadConfig(); $this->loadDB(); $this->loadCache(); $this->loadMongo(); } public function getParams($argv) { $params = []; array_shift($argv); foreach ($argv as $k => $v) { $tmpArr = explode("=", $v); $params[reset($tmpArr)] = end($tmpArr); } return $params; } public function getBasePath() { return ROOT_PATH; } public function autoLoadClass() { //自动加载第三方扩展类库 require_once ROOT_PATH.'vendor/autoload.php'; //自动加载命名空间下扩展库 spl_autoload_register(function($className) { $currentFile = $this->basePath. str_replace("\\","/",$className) .".php"; //echo "try to auto load ".$currentFile.PHP_EOL; if (file_exists($currentFile)) { require_once "{$currentFile}"; } else{ throw new \Exception("class {$className} is not exist!"); } }); } public function loadConfig(){ $confPath = $this->basePath."config".DIRECTORY_SEPARATOR; $baseData = require_once $confPath."config.php"; $envData = require_once $confPath.APPLICATION_ENV.".php"; Config::set(array_merge($baseData,$envData)); } public function loadDB(){ $db = new DB; $dbConf = Config::get('db')['mysql']; $db->addConnection([ 'driver' => $dbConf['driver'], 'host' => $dbConf['host'], 'database' => $dbConf['database'], 'username' => $dbConf['user'], 'password' => $dbConf['pwd'], 'charset' => 'utf8', 'collation' => 'utf8_unicode_ci', 'prefix' => $dbConf['prefix'], ]); $db->setAsGlobal(); $db->bootEloquent(); } public function loadCache(){ $rdsConf = Config::get('redis'); RedisTool::connect($rdsConf); } public function loadMongo(){ $mongoConf = Config::get('mongo'); MongoTool::connect($mongoConf); } }
这个抽象类是所有自动任务类的父类并定义了run()抽象方法,方便子类通过run()方法实现各自业务逻辑。本类核心功能在构造器中实现,除了参数和路径解析外,最重要的方法就是bootstrap(),这个函数做了以下初始化工作:
- autoLoadClass()
- loadConfig()
- loadDB()
- loadCache()
- loadMongo()
autoLoadClass方法,顾名思义就是加载类文件,这个方法首先执行解决类加载问题,这里要区分根目录下console.php中的类加载,这里主要是对类的依赖做了加载,包括加载第三方类库和核心类库文件,保障本类引用的命名空间类类文件均可正常加载。
loadConfig方法,顾名思义就是加载配置文件,初始化配置信息,为程序下一步做准备
loadDB方法,顾名思义就是加载数据库实例,这里加载了流行的laravel DB 组件
loadCache方法,顾名思义就是加载缓存实例,这里加载了php-redis缓存类库
loadMongo方法,顾名思义就是加载MongoDB实例,这里前面已有介绍
至此整个自动任务实例类完成了以上流程初始化后,就可以回到console.php流程中getInstance方法,进行实例对象进行业务处理了
以index.php文件为例说明:
<?php define("PROJECT_PATH",str_replace(str_replace("\\","/",__NAMESPACE__),"",__DIR__)); define("CONTROL_PATH",PROJECT_PATH.DIRECTORY_SEPARATOR."App".DIRECTORY_SEPARATOR."Controller".DIRECTORY_SEPARATOR); /** * @stat php index.php * @http http://127.0.0.1:9001/?r=Index/Index/Index&uid=1 */ use \Swoole\Http\Server AS WebServer; class Server { public $route; public $params; public $defaultpt = 9001; public $defaultrt = "Index/Index/Index"; public $defaultns = "App\\Controller\\"; public function parseRoute() { if(empty($this->params['r'])) $this->params['r'] = $this->defaultrt; $tmpArr = explode("/",$this->params['r']??''); if (count($tmpArr) !=3) { throw new \Exception("invalid route params!"); } array_walk($tmpArr ,function(&$itm) { $itm = ucfirst($itm); }); $this->route = [ 'module' => $tmpArr[0], 'controller' => $tmpArr[1], 'method' => $tmpArr[2] ]; //去掉特殊路由参数 unset($this->params['r']); } //auto_laod class public function dispath() { $basePath = CONTROL_PATH."Base.php"; $scriptPath = CONTROL_PATH. sprintf("%s%s%sController",$this->route['module'],DIRECTORY_SEPARATOR,$this->route['controller']).".php"; foreach([$basePath,$scriptPath] as $r) { if (!file_exists($r)) { throw new \Exception("file {$r} is not exist!"); } require_once $r; } } public function getInstance(){ $className = sprintf("%s%s\\%sController",$this->defaultns,$this->route['module'],$this->route['controller']); $actionName = sprintf("%sAction",$this->route['method']); if (!class_exists($className)) { throw new \Exception("class name:".$className." is not exist!!"); } if (!method_exists($className,$actionName)) { throw new \Exception("method name:".$actionName." is not exist!"); } if(!Context::has($className)){ $instance = new $className(); Context::set($instance); } else{ $instance = Context::get($className); } $instance->setParams(array_merge($this->params,$this->route)); $res = $instance->{$actionName}(); return $res; } public function start() { $http = new WebServer("0.0.0.0", $this->defaultpt); $http->on('request', function ($request, $response) { try{ $this->params = array_merge($request->get??[],$request->post??[]); $this->parseRoute(); $this->dispath(); $resData = $this->getInstance(); $response->header("Content-Type", "application/json; charset=utf-8"); }catch(\Throwable $e) { $response->header("Content-Type", "text/html; charset=utf-8"); $resData = $e->getMessage(); } $response->end($resData); }); $http->start(); } } class Context{ public static $map; public static function set($instance) { $className = get_class($instance); if(!self::has($className)){ self::$map[$className] = $instance; } } public static function has($className) { return !empty(self::$map) && array_key_exists($className,self::$map); } public static function get($className) { if (self::has($className)) { return self::$map[$className]; } return null; } } $sv = new Server(); $sv->start();
controller整个流程和console类似,不同之处在于,controller引入了基于Swoole的http服务器,采用常驻的 cli 运行模式,每次请求不用加载全部项目代码,效率更高。因此新增了Context对象方便请求实例的复用和核心类的挂载。
启动web服务器
php index.php
访问路由控制器
curl -XGET http://127.0.0.1:9001/?r=user/index/index&uid=1
介绍常用部署方法,需要根据业务场景合理选择,包括本地部署和docker容器部署二种方式,下面分别介绍
本地部署需要在本地安装php版本推荐7.1以上,同时需要安装swoole扩展,mongodb扩展和redis扩展等,安装完成后,进入项目根目录,安装依赖即可执行。
php安装比较基础,这里省略
进入http://pecl.php.net/package/mongodb 选择合适的适配版本,完整安装命令如下
wget http://pecl.php.net/get/mongodb-1.6.0.tgz cd /mongodb-1.6.0 phpize ./configure make && make install
安装完毕后,根据安装结果提示,将mongodb.so扩展文件引入到php配置文件下即可
进入http://pecl.php.net/package/swoole 选择合适的适配版本,完整安装命令如下
wget http://pecl.php.net/get/swoole-4.5.0.tgz cd /swoole-4.5.0 phpize ./configure make && make install
安装完毕后,根据安装结果提示,将swoole.so扩展文件引入到php配置文件下即可
进入http://pecl.php.net/package/swoole 选择合适的适配版本,完整安装命令如下
wget http://pecl.php.net/get/redis-2.0.0.tgz cd /redis-2.0.0 phpize ./configure make && make install
安装完毕后,根据安装结果提示,将redis.so扩展文件引入到php配置文件下即可
curl -sS https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer安装完成composer后,进入项目根目录安装依赖:
sudo composer install
如果只是用来跑自动任务的场景,执行如下:
php console.php r=mysql/testMysql uid=1
如果是用来跑API接口或者web页面,需要开启http服务,再访问页面路由:
php index.php #开启http服务
也可同时执行自动任务和进行web服务,根据自己实际项目选择即可
项目支持在docker容器环境下打包运行,本机需要提前安装好docker环境,在完成打包镜像和开启容器后,会自动开启web服务,容器启动成功后可直接在宿主机下进行页面访问
构建镜像时注意项目所在目录路径,示例项目目录为/disks/F/php-console-v2,进入项目目录
sudo docker build -t php-console:v2 .sudo docker run --name php-console-v2 -p 9001:9001 -v /disks/F/php-console-v2:/opt/www:rw --restart=always -d php-console:v2
进入容器内部,执行脚本入口文件
sudo docker exec -it php-console-v2 /bin/bash
php console.php r=mysql/testMysql uid=1sudo docker logs -f --tail 100 php-console-v2
宿主机更新代码后,需要手动重启容器
sudo docker restart php-console-v2
项目支持通过docker-compose 方式,配置好容器内容编排后,使用docker-compose命令,一键部署,非常方便,强烈推荐!具体步骤如下:
#下载可执行文件 sudo curl -L "https://github.com/docker/compose/releases/download/1.29.1/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose #设置可执行权限 sudo chmod +x /usr/local/bin/docker-compose #设置快捷方式 sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose #查看版本 docker-compose --version
支持按需求自定义容器配置,比如端口映射,指定容器镜像,指定挂载目录等,具体配置如下:
version: '3' services: php-console-v2: container_name: php-console-v2 image: php-console:v2 build: context: . dockerfile: Dockerfile volumes: - ./:/opt/www:rw ports: - 9001:9001 restart: always environment: - APP_ENV=dev - SCAN_CACHEABLE=false networks: default: name: php-console-v2
sudo docker-compose build
sudo docker-compose up -d
输入 http://127.0.0.1:9001/ 查看是否正常输出记过,如果报错提示找不到加载文件,则需要进入容器安装依赖
sudo docker exec -it php-console-v2 /bin/bash cd /opt/www && compose install
以上简单介绍了整个项目细节,由于时间仓促,项目本身还存在一定不完善的地方,可根据自己实际情况进行优化和重写,比如视图这块写的很简单,没有引入模板引擎等,比如没有专门日志接口等,读者可自行实现完善。
提供项目下载地址,飞书下载地址如下:
模块名称保持首字母大写
方法名首字母小写,并遵守驼峰法命名
控制器遵守驼峰法,首字母大写,类名和文件名保持一致
自动任务遵守驼峰法,首字母大写,类名和文件名保持一致
类库文件遵守驼峰法,首字母大写,类名和文件名保持一致
-
项目启动前,先根据实际配置好对应的数据库缓存连接
-
项目中根据业务情况合理拆分服务
-
Context的应用场景,推荐在控制器中使用,不要在类库和服务中直接使用,否则同时使用自动任务时会出错
-
控制器和自动任务相关服务做好兼容复用
-
未完待续