# TOP-Framework *部分代码于九年前所写,后经过一系列重构,形成的一套框架。在此准备写一个文档。* ## 目录结构 遵循PSR-2规范的编码风格,遵循PSR-4自动加载规范。 基本目录结构: ``` -application 应用目录 -framework 框架所在目录 -config 默认配置文件目录 -create 自动生成模块 -extend 功能扩展类目录 -library 核心类库目录 -cache 缓存具体实现 -database 数据库操作具体实现 -error 错误处理 -exception 异常处理 -functions 框架函数库 -http 请求/响应类 -template 模板引擎具体实现 -...... 实际调用的类 -middleware 默认中间件 -traits 通用trait -vendor composer加载的类库 -public 可访问公共资源 ``` ## 入口文件 ### 入口文件中的配置 ``` use \top\Framework; require '../framework/Framework.php'; // 可能你会使用到下面这些配置 // 调试模式,缺省值:false // Framework::debug(true); // 可使用常量DEBUG取得该值 // 项目目录,缺省值:./application/ // Framework::appPath('../application/'); // 可使用常量APP_PATH取得该值 // 项目命名空间,缺省值:app // Framework::appNameSpace('app'); // 可使用常量APP_NS取得该值 // session保存目录,缺省值:./runtime/session/ // Framework::sessionPath('./runtime/session/'); // 可使用常量SESSION_PATH取得该值 // 框架目录,缺省值:Framework.php的绝对路径 // Framework::frameworkPath('../framework/'); // 可使用常量FRAMEWORK_PATH取得该值 // 静态资源目录,缺省值:/resource/ // Framework::resourcePath('/resource/'); // 可使用常量RESOURCE取得该值 // 默认模块,缺省值:home // Framework::defaultModule('home'); // 可使用常量DEFAULT_MODULE取得该值 // 绑定模块,缺省值:home // Framework::bindModule('home'); // 可使用常量BIND_MODULE取得该值,定义该常量后,每个模块需要单独入口文件 // 启动应用 Framework::appPath('../application/'); Framework::bindModule('home'); Framework::startApp(); ``` ## 模块 ### 创建模块 1. 手动创建 在application(可更改)目录下创建home目录,再创建config、controller(必须)、model、view目录 2. 命令行自动创建 ``` php create.php 目录 模块名 [入口文件] ``` 进入根目录,执行以下命令: ``` php create.php application home index ``` 至此,已经通过简单的命令创建了home模块,浏览器输入127.0.0.1即可访问home模块(系统默认模块为home模块,可在入口文件中使用Framework::bindModule()指定)。 ## 控制器 ### 创建控制器 一个典型的控制器(Index.php) ``` view(); } ``` 调用基础控制器中的view方法、并return出去,完成模板的展示。 ### 模板传值 1. view方法/view函数 ``` public function index() { return $this->view(null, [ 'param' => 'Hello world!' ]); return view(null, [ 'param' => 'Hello world!' ]); } ``` 2. view_param函数 ``` public function index() { view_param([ 'param' => 'Hello world!', ]); return $this->view(); } ``` 3. 直接return数组 ``` public function index() { return [ 'param' => 'Hello world!' ]; } ``` 以上两种方式等效。 ### 参数自动注入 ``` namespace app\home\controller; use app\home\model\Users; use top\library\http\Request; class Index { protected $users = null; public function __construct(Users $users) { $this->users = $users; } public function index(Request $request, Users $users) { return 'Hello'; } } ``` 可在参数中指定所有可以被实例化(外部new或外部静态调用instance方法)的类,将自动将类实例化并注入到方法中。 ### 前置与后置操作 前、后置操作仅支持使用注解配置,多个方法可使用|隔开。方法名称必须以_开头,如果前置方法return的值为真,则不会继续执行。 ``` /** * @beforeAction _before * @afterAction _after */ public function index() { return 'Hello'; } public function _before() { // 前置操作 } public function _after() { // 后置操作 } ``` 这部分的实现代码是放在默认Action中间件中的,后面可能会改。 ## 模板 框架自带一款模板引擎,暂时命名为TOP模板引擎。此外支持扩展其他第三方模板引擎,后面会讲到,先来看看自带模板引擎的基础使用。 ### 模板继承 模板继承通过extend标签与block标签配合使用实现。 一个最简单的继承 ``` // Base/layout.html(父级模板) Title // Index/index.html

内容主体

``` ### 模板标签 内置一些常用标签 1. php php标签。此标签为闭合标签,标签内的内容将被解析为原生php代码执行。 ``` echo '你好'; ``` 2. if if标签。此标签为闭合标签,condition属性为if的条件,属性列表:condition。 ``` // do something... ``` 3. else else标签。此标签为自闭合标签,可选属性condition,存在condition属性则被解析为elseif,属性列表:condition(可选)。 ``` // do something... // do something... // do something... // do something... ``` 4. loop 循环标签。此标签为闭合标签,属性列表:name、id、key(可选)。 ``` {item.id} {i}、{item.id} ``` 5. assign 赋值标签,在模板中创建新的php变量。此标签为自闭合标签,属性列表:name、value。 ``` ``` 6. original 此标签为闭合标签。original标签中的内容不会被编译。 ``` {item.id} ``` 上例,loop标签会被原样输出。 7. switch 此标签为闭合标签。 ``` $name的值为0 $name的值为1 默认 ``` 8. include 在当前模板中加载其他模板文件。 9. 变量、函数调用 ``` // 变量输出 {username} // 函数调用需要在左定界符后加上: {:mb_substr($username, 0, 3, 'utf8')} ``` ### 自定义标签 新建自定义标签库类文件/application/home/tags/Extend.php,目录及文件名称没有要求。自定义标签会以类名进行分组。 #### 闭合标签 ``` namespace app\home\tags; class Extend { public $tags = [ 'test' => ['attr' => 'start,length,id', 'close' => 1] ]; public function _test($attr, $content) { $parse = ''; $parse .= $content; $parse .= ''; return $parse; } } ``` 类创建完成后,到配置文件config.php的view下的tagLib中添加Extend类 ``` 'view' => [ 'tagLib' => [ \app\home\tags\Extend::class ] ], ``` 添加完成后即可在模板中使用 ``` {$test} ``` #### 自闭合标签 添加一个描述 ``` 'say' => ['attr' => 'what', 'close' => 0] ``` 新建_say方法 ``` public function _say($attr) { return ""; } ``` 模板调用 ``` ``` ### 模板缓存 #### 实现 在渲染模板后,可选择将渲染结果缓存下来(文件缓存)。在框架调用控制器之前有面向控制器的前置操作,在此会判断是否存在模板缓存文件,如果存在并且有效,则会直接使用缓存文件。否则,将会重新渲染模板。 #### 使用 1. 在view方法中使用。第三个参数为缓存控制,传入的参数为true时使用配置文件中设置的全局缓存时间,传入数字则表示当前模板的缓存时间(秒) ``` return $this->view('Index/index', [ 'data' => $data ], 30); ``` 2. 直接在控制器中调用cache方法。参数作用参照view方法。 ``` $this->cache(30); return [ 'data' => $data ]; ``` ### 第三方模板引擎 文件存放位置 'framework/library/template/driver' 。必须实现TemplateIfs接口,所以需要实现以下三个方法: 1. run 返回当前类实例,做模板引擎初始化操作。 2. cache 设置缓存状态。 3. fetch 返回模板渲染后的内容。 驱动类编写完成后需要以下两个步骤,方可使用(以自带模板引擎的配置为例): 1. 配置文件中注册 ``` 'register' => [ 'Top' => \top\library\template\driver\Top::class, ], ``` 2. 模板配置中配置使用 ``` 'view' => [ 'engine' => 'Top', ], ``` ## 模型 ### 创建模型 一个典型的模型(Users.php) ``` update([ 'username' => 'TOP糯米' ], 1); ``` 除此之外,提供另一种方式,传递更多条件或更复杂的条件 ``` $this->update([ 'username' => 'TOP糯米' ], function ($db) { $db->where('id', 1); }); ``` 当然,也可以使用连贯操作 ``` $this->where('id', 1)->update([ 'username' => 'TOP糯米' ]); ``` 成功返回受影响的记录数,失败抛出DatabaseException异常。 4. find($param = false, $notRaw = true) 查找一条记录,方法get可使用静态调用 可传入第一个参数为主键,第二个参数为是否按指定的规则(outReplace属性)进行处理。 一般调用 ``` $this->find(1); ``` 匿名函数 ``` $this->find(function ($db) { $db->where('id', 1); }); ``` 连贯操作 ``` $this->where('id', 1)->find(); ``` 成功返回一个一维数组,失败抛出DatabaseException异常。 5. select($param = false, $notRaw = true) 查找多条记录,方法all可使用静态调用 使用方法同find 成功返回一个二维数组,失败抛出DatabaseException异常。 6. delete 删除记录 直接传入主键 ``` $this->delete(1); ``` 匿名函数 ``` $this->delete(function ($db) { $db->where('id', 1); }); ``` 连贯操作 ``` $this->where('id', 1)->delete(); ``` 成功返回受影响的记录数,失败抛出DatabaseException异常。 7. count 返回记录数 一般调用 ``` $this->count(); ``` 第一个参数同样可以为匿名函数、并且同样支持连贯操作 成功返回记录数,失败抛出DatabaseException异常。 8. avg 计算平均值 接收一个参数,当没有使用field方法指定字段时,可直接传入字段名,以计算平均值。 ``` $this->avg('score'); ``` 使用field方法指定字段 ``` $this->field('score')->avg(); ``` 匿名函数中指定字段或条件 ``` $this->avg(function ($db) { $db->where('score', '>=', 60); $db->field('score'); }); ``` 成功返回平均值,失败抛出DatabaseException异常。 9. max 计算最大值 同avg方法 10. min 计算最小值 同avg方法 11. sum 求和 同avg方法 12. sql 返回最后执行的SQL语句,可使用静态调用 13. distinct 过滤记录中的重复值,可使用静态调用 接收一个为字段名称的参数 调用 ``` $this->distinct('sex')->select(); ``` 失败抛出DatabaseException异常。 14. field 指定字段,可使用静态调用 可传入字符串或数组 15. where 指定条件,可使用静态调用 最多可接收三个参数 仅传入一个参数时,可传入字符串或数组 ``` $where = ['id' => ['>', 10], 'sex' => 1]; $where = 'id > 10 and sex = 1'; $this->where($where)->select(); ``` 两个参数,解析为“字段=值” ``` $this->where('sex', 1)->select(); ``` 三个参数,指定字段与字段值的连接符 ``` $this->where('sex', '>', 1)->select(); ``` 16. order 对结果进行排序,可使用静态调用 ``` $this->order('id desc')->select(); ``` 也可以使用匿名函数调用order方法 17. limit 查询范围,可使用静态调用 接收一个参数,可以是字符串或数组 一般调用 ``` $this->limit('0, 5')->select(); $this->limit([0, 5])->select(); ``` 两种方式等效 18. alias 指定当前表别名,可使用静态调用 具体请看join演示 19. join 加入多表进行查询,可使用静态调用 接收三个参数,第一个参数为连接方式(空、left、right、full),第二个参数为表名(不包含前缀),第三个参数为别名(当前表会自动将”this“作为别名)。 一般调用 ``` $this->alias('t')->select(function ($db) { $db->join('score s', 's.uid = t.id', 'left'); }); ``` 同样也可以使用连贯操作 20. transaction 事务处理 ``` use app\home\model\Users; $userModel = model(Users::class); $res = $userModel->transaction(function ($db) { $db->delete(4); $db->update([ 'id' => 3, ], 1); }); var_dump($res); ``` 上例中,开启了一个事务,先删除一条记录,再更新一条记录的ID为已存在的ID,更新操作必定不会执行成功,所以数据将会执行回滚,删除数据也不会执行成功,var_dump($res)结果为false。返回值为布尔值,成功返回true,失败返回false。transaction方法接收一个匿名函数,匿名函数形参为当前模型。SQL执行失败时会回滚事务,也可以通过手动抛出DatabaseException异常来回滚事务。 21. getPDO 获取PDO对象 使用该方法获取到PDO对象,可以自行使用PDO操作数据库 #### 属性 1. $table 指定当前模型的表名(优先于模型名称) ``` protected $table = 'users'; ``` 2. $pk 指定当前模型的主键(如果不指定,程序将自动查询以当前模型命名的表的主键) ``` protected $pk = 'id'; ``` 3. $map 指定传入数据的键与数据库字段名称的映射关系 ``` protected $map = [ 'name' => 'username' ]; ``` 4. $prefix 指定当前表前缀 ``` protected $prefix = 'cms_'; ``` 5. $insertReplace 入库时替换值 数据入库时自动格式化时间为unix时间戳 ``` protected $insertReplace = [ 'create_time' => ['formatTime', true] ]; protected function formatTime($time) { return strtotime($time); } ``` 至此,在数据在被写入数据库之前会先调用inReplace中设定的函数、方法,并将return的值作为新的值写入数据库。 注意:当以字段为键名的数组的值为一个字符串时,则该字符串为即将调用的函数,如果值为一个数组,且无第二个值或第二个值为false、空,则该数组第一个值为即将调用的函数,如第二个值为true,则表示当前调用的方法存在于本类或父类中。 6. $outReplace 出库时替换值 ``` protected $outReplace = [ 'sex' => ['outFormatSex', true] ]; protected function outFormatSex($sex) { switch ($sex) { case 1: return '男'; case 2: return '女'; default: return '未知'; } } ``` 注意:当以字段为键名的数组的值为一个字符串时,则该字符串为即将调用的函数,如果值为一个数组,且无第二个值或第二个值为false、空,则该数组第一个值为即将调用的函数,如第二个值为true,则表示当前调用的方法存在于本类或父类中。 7. $updateReplace 数据更新时替换值 基本类似于inReplace,但仅当执行更新操作时执行。 8. $validate 自动验证 验证不为空 ``` protected $validate = [ 'username' => ['notNull', '用户名不能为空'], ]; ``` 验证不等于 ``` protected $validate = [ 'username' => ['notEqual', '0', '用户名不能为0'], ]; ``` 多条件(用户名不为空且不为0) ``` protected $validate = [ 'username' => [ ['notNull', '用户名不能为空'], ['notEqual', '0', '用户名不能为0'], ], ]; ``` 自定义函数验证,新建函数demo到函数库 ``` function demo($name, $n1, $n2) { if ($name == $n1 || $name == $n2) { return false; } return true; } ``` 添加到自动验证 ``` protected $validate = [ 'username' => [ ['notNull', '用户名不能为空'], ['notEqual', '0', '用户名不能为0'], ['demo', 'TOP糯米', '张三', '用户名不能为TOP糯米或张三'], ], ]; ``` ### 调用模型 调用模型有两种方式: 1. model函数(推荐) model函数会返回一个模型的单例,使用方式与直接new无差别。 ``` $object = model(模型); ``` 2. new 模型 ``` $object = new 模型(); ``` ## 缓存 缓存类必须实现CacheIfs接口,所以需要实现以下四个方法: 1. set 设置缓存。三个参数,前两个为必须,第三个默认为10(秒),第一个参数为准备缓存数据的key,第二个参数为缓存的具体数据(字符串、数组、对象),第三个为当前缓存的有效时间。 ``` $cache->set('lists', [0, 1, 2, 3, 4, 5], 30); ``` 2. get 根据key获取缓存内容 ``` $cache->get('lists'); ``` 第一个参数为缓存标识,第二个可选参数为当前缓存不存在即将调用的匿名函数,并将返回值当作第一次调用的缓存值。 3. remove 根据key删除缓存 ``` $cache->remove('lists'); ``` 4. exists 根据key判断缓存是否存在/有效 ``` $cache->exists('lists'); ``` ### File 1. 使用判断设置缓存 ``` use top\library\cache\driver\File; $cache = File::instance(); if (!$cache->exists('text')) { $text = '测试'; $cache->set('text', $text); } $data = $cache->get('text'); ``` 2. get方法 ``` use top\library\cache\driver\File; $cache = File::instance(); $data = $cache->get('text', function ($cache) { $cache->set('text', $text = '测试'); return $text; }); ``` ### Redis 使用方式同File缓存 ### 自定义缓存实现 文件存放位置 'framework/library/cache/driver' 。必须实现CacheIfs接口,具体方法看缓存介绍。 ## 路由 可在config.php配置文件中使用的配置 ``` 'compel_route' => false, // 是否开启强制路由 'complete_parameter' => true, // 是否开启完整参数 ``` ### 默认路由 访问方式 http://yourdomain/控制器/方法 开启完整参数,则传递参数需要使用完整名称,如方法有参数id,则需要将id在链接中体现 http://yourdomain/Users/edit/id/1 关闭完整参数,可以直接跟参数值 http://yourdomain/Users/edit/1 ### 强制路由 开启强制路由后,默认路由方式将全部失效,仅手动配置(配置文件、注解)的路由可用。 ### 路由配置 #### 方法注解 假设有控制器Index ``` class Index { /** * 首页 * @route /home * @requestMethod GET */ public function index() { return 'Hello'; } } ``` 给index方法添加注解@route为/home,则可以直接通过 http://yourdomain/home 访问到对应位置,@requestMethod注解为请求方法,如果不指定请求方法,则任何方法都可访问。 #### 配置文件 路由配置文件位于模块config目录下,文件名:route.php ``` return [ 'get' => [ '/home' => [ 'class' => app\home\controller\Index::class, 'method' => 'index', ], ], ]; ``` 进行以上配置后,可以通过GET方式请求 http://yourdomain/home 访问到对应位置。可配置的组有any、其他请求方法,其中any为不限制,任何方法都可访问,路由配置文件中可以指定执行、不执行的中间件,accept_middleware为执行的中间件,except_middleware为不执行的中间件,如下: ``` return [ 'get' => [ '/home' => [ 'class' => app\home\controller\Index::class, 'method' => 'index', 'accept_middleware' => [ app\home\middleware\Auth::class ], 'except_middleware' => [], ], ], ]; ``` ## 其他 ### Database类 模型中的数据库操作实际也是调用Database类中的方法,模型类是在此基础上新增了更多高级操作,Database类方法的使用请参照模型的使用。在此需要特别指出,获取Database类实例使用table方法,table方法中传入表名以指定即将操作的数据表,例: ``` $db = Database::table('users'); $data = $db->find(1); ``` 框架使用PDO操作数据库。此外,支持自定义数据库操作驱动类,文件位置 'framework/library/database/driver' ,必须实现连接数据库、获取表主键方法,可以继承Base类,拥有通用的增删改查方法。此外,可以实现DatabaseIfs接口,包括以下方法: 1. connect 使用PDO连接数据库。 2. getPk 获取表主键。 #### 注意 Database类仅提供基本的增删改查,不会有模型中的入库、出库值替换,也不会有数据验证等操作,查询使用方式与Model类基本相同。Database类的事务与Model类不同,Model类进行了更进一步的封装。Database类事务使用示例: ``` use top\library\Database; $db = Database::table('users'); // 开启事务 $db->begin(); try { // 一些对数据库的改动操作 // 提交 $db->commit(); } catch (DatabaseException $exception) { // 回滚 $db->rollback(); // 其他操作 } ``` ### Request类 #### 获取实例 ``` Request::instance(); // 或者 request(); ``` #### 方法 1. is 判断请求方法 ``` // 判断是否为POST请求 request()->is('post'); // 判断是否为GET请求 request()->is('get'); ``` 2. create 创建一个HTTP请求 原型:create($url, $data = [], $header = []) 第一个参数为请求的链接,第二个参数为将要POST的数据,第三个参数为指定Header参数 3. ip 返回客户端IP地址 4. module 当前请求的模型名称 5. controllerFullName 当前请求的完整控制器名称 6. controller 当前请求的不包含命名空间的控制器名称 7. method 当前请求的方法名称 8. params 当前请求所带的参数 9. get 获取get数据 原型:get($name = '*', $except = [], $filter = 'filter') 第一个参数为将要获取的变量名称(' * ' 为全部),第二个参数为过滤的变量,第三个参数为指定的过滤函数(可以为自定义函数名称或匿名函数)。 函数名称: ``` request()->get('id', ['type'], 'filter'); ``` 匿名函数: ``` request()->get('id', ['type'], function ($value) { return (int) $value; }); ``` 10. post 获取post数据 使用同get方法 11. header 获取请求中header数据 使用同get方法 12. except 指定过滤的变量 取出全部get数据,但不包括type ``` request()->except('type')->get(); ``` ### Response类 #### 获取实例 ``` Response::instance(); // 或者 response(); ``` #### 响应内容 ``` return $response->code(200)->header([ 'X-Powered-By: 测试', ])->send('OK'); ``` 其中,header方法接收数组或字符串的参数,参数为具体的响应头内容。send方法参数为具体响应内容。 ### 中间件 ### 创建中间件 创建application/home/middleware/Auth.php测试文件 ``` namespace app\home\middleware; use top\middleware\ifs\MiddlewareIfs; use top\library\http\Request; class Auth implements MiddlewareIfs { public function handler(Request $request, \Closure $next) { if (true) { return '拒绝请求'; } return $next(); } } ``` #### 使之生效 ##### 方法注解 ``` /** * @acceptMiddleware app\home\middleware\Auth */ public function index() { } ``` ##### 配置文件 ``` 'middleware' => [ app\home\middleware\Auth::class ], ``` 配置文件中的配置的中间件是全局有效的。使用注解,可以为方法指定执行的中间件,同样也可以为方法指定不执行的中间件,只需要加上@exceptMiddleware注解即可,如下: ``` /** * @exceptMiddleware app\home\middleware\Auth */ public function index() { } ``` ### config.php 以home模块为例,文件位置 'application/home/config/config.php'。此外还存在一个默认配置文件,文件位置 'framework/config/config.php',如果用户存在同名配置,将会执行merge操作。 如果需要配置数据库,将如下内容添加到应用配置文件中 ``` 'db' => [ 'driver' => 'Mysql', 'host' => '127.0.0.1', 'user' => '', 'passwd' => '', 'dbname' => '', 'prefix' => '', 'port' => 3306, 'charset' => 'utf8' ], ``` 如果需要其他配置,请到framework/config/config.php中查看其他的配置。 ### Config类 Config类用于获取、设置配置。 #### 获取实例 ``` Config::instance(); ``` #### 动态配置 ``` $instance->set('appid', '1234566'); ``` #### 获取配置 ``` $instance->get('appid'); ``` ### Composer 请配置vendor目录到framework/vendor。