很久没有碰PHP代码额,作为阅读记录吧。
XiunoBBS
由来 最近因为特殊原因想建立一个社区(论坛)有某些作用,机缘巧合下找到xiuno bbs,看过上面的开发手册后,感觉很有利于二次开发,所以好久没看PHP的我重新捡起来预习 下,记录下整个流程加载。
手册中很多内容写的很详细,不做重新复述。
结构 XiunoBBS目录结构 v4.0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 admin/ -- 后台管理目录 route -- 后台路由 view -- 后台模板 index.php -- 后台入口 index.inc.php -- 入口包含的代码段 admin.func.php -- 后台依赖的函数 menu.conf.php -- 后台配置文件 conf/ -- 全站配置文件 conf.php -- 配置文件 conf.default.php -- 默认配置文件 attach.conf.php -- 附件配置文件 smtp.conf.php -- 发送邮件的配置文件 install/ -- 安装目录 lang/ -- 语言包 log/ -- 日志目录,按照天存放日志 model/ -- 数据处理的函数文件目录 plugin/ -- 插件目录,一个插件一个目录 route/ -- 路由目录,业务逻辑处理 tmp/ -- 临时文件存放目录,插件和代码合并后的文件存放于此 upload/ -- 上传目录 view/ -- 前端模板目录 robots.txt -- 屏蔽蜘蛛的配置文件 .htaccess -- Apache URL-Rewrite 文件 index.inc.php -- 前端入口包含代码段 model.inc.php -- Model 包含目录 index.php -- 前台程序入口
hook位置 1 2 3 4 5 6 // hook xiunophp_include_before.php xiunophp包含前 // hook xiunophp_include_after.php xiunophp包含完成后 // hook index_inc_start.php index配置开始 // hook index_inc_end.php index配置结束 // hook index_inc_route_before.php 路由开始前 ……
hook的位置太多了。。
override位置 1 2 3 4 5 6 7 8 9 index.inc.php view/htm/*.htm route/*.php model/*.php admin/view/htm/*.htm admin/route/*.php admin/index.inc.php admin/menu.conf.php lang/*.php
这些都可以被override
加载 在 Xiuno BBS 4.0 当中,采用的单入口设计,全部从 index.php 进。 所有的 xxx-xxx.htm 都通过 Web Server 转发到了 index.php?route-action.htm。 由 route 目录下对应的 php 文件进行处理(Controller 层)。 model 则为数据处理目录(Model 层)。 view 为 js css font 等负责显示的文件 目录(View 层)。
入口 文档中所说是从index.php唯一入口进入,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 <?php !defined('DEBUG' ) AND define('DEBUG' , 0 ); define('APP_PATH' , dirname(__FILE__ ).'/' ); !defined('ADMIN_PATH' ) AND define('ADMIN_PATH' , APP_PATH.'admin/' ); !defined('XIUNOPHP_PATH' ) AND define('XIUNOPHP_PATH' , APP_PATH.'xiunophp/' ); $conf = (@include APP_PATH.'conf/conf.php' ) OR exit ('<script>window.location="install/"</script>' ); !isset ($conf['user_create_on' ]) AND $conf['user_create_on' ] = 1 ; !isset ($conf['logo_mobile_url' ]) AND $conf['logo_mobile_url' ] = 'view/img/logo.png' ; !isset ($conf['logo_pc_url' ]) AND $conf['logo_pc_url' ] = 'view/img/logo.png' ; !isset ($conf['logo_water_url' ]) AND $conf['logo_water_url' ] = 'view/img/water-small.png' ; $conf['version' ] = '4.0.4' ; substr($conf['log_path' ], 0 , 2 ) == './' AND $conf['log_path' ] = APP_PATH.$conf['log_path' ]; substr($conf['tmp_path' ], 0 , 2 ) == './' AND $conf['tmp_path' ] = APP_PATH.$conf['tmp_path' ]; substr($conf['upload_path' ], 0 , 2 ) == './' AND $conf['upload_path' ] = APP_PATH.$conf['upload_path' ]; $_SERVER['conf' ] = $conf; if (DEBUG > 1 ) { include XIUNOPHP_PATH.'xiunophp.php' ; } else { include XIUNOPHP_PATH.'xiunophp.min.php' ; } include APP_PATH.'model/plugin.func.php' ;include _include(APP_PATH.'model.inc.php' );include _include(APP_PATH.'index.inc.php' );?>
定义了APP_PATH,ADMIN_PATH,XIUNOPHP_PATH的目录
包含APP_PATH应用目录下的配置目录下的配置文件conf/conf.php,这里就代表引入了配置
将log、upload、tmp目录转换成绝对路径
把配置conf赋值给server数组
然后加载xiunophp,待会再看里面有哪些东西
包含plugin.func.php,加载插件某些函数
包含model.inc.php
包含index.inc.php
最后这几个包含都挺有内容的,挨个来查看
xiunophp 引入了缓存类、数据库支持类、还有一些自定义封装的函数,然后初始化某些内容,具体可以看代码中如何书写。
最后往server超全局变量中存储了这些内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $_SERVER['starttime' ] = $starttime; $_SERVER['time' ] = $time; $_SERVER['ip' ] = $ip; $_SERVER['longip' ] = $longip; $_SERVER['useragent' ] = $useragent; $_SERVER['conf' ] = $conf; $_SERVER['lang' ] = $lang; $_SERVER['errno' ] = $errno; $_SERVER['errstr' ] = $errstr; $_SERVER['method' ] = $method; $_SERVER['ajax' ] = $ajax; $_SERVER['get_magic_quotes_gpc' ] = $get_magic_quotes_gpc; $_SERVER['db' ] = $db; $_SERVER['cache' ] = $cache;
plugin.func.php plugin.func.php这个文件中定义了有关插件的各种函数,在这里不一一列举。是为后面加载插件做准备。
其中有部分函数经常会调用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 function _include ($srcfile) { global $conf; $len = strlen(APP_PATH); $tmpfile = $conf['tmp_path' ].substr(str_replace('/' , '_' , $srcfile), $len); if (!is_file($tmpfile) || DEBUG > 1 ) { $s = plugin_compile_srcfile($srcfile); $g_include_slot_kv = array (); for ($i = 0 ; $i < 10 ; $i++) { $s = preg_replace_callback('#<template\sinclude="(.*?)">(.*?)</template>#is' , '_include_callback_1' , $s); if (strpos($s, '<template' ) === FALSE ) break ; } file_put_contents_try($tmpfile, $s); $s = plugin_compile_srcfile($tmpfile); file_put_contents_try($tmpfile, $s); } return $tmpfile; } function plugin_compile_srcfile ($srcfile) { global $conf; if (!empty ($conf['disabled_plugin' ])) { $s = file_get_contents($srcfile); return $s; } $srcfile = plugin_find_overwrite($srcfile); $s = file_get_contents($srcfile); for ($i = 0 ; $i < 10 ; $i++) { if (strpos($s, '<!--{hook' ) !== FALSE || strpos($s, '// hook' ) !== FALSE ) { $s = preg_replace('#<!--{hook\s+(.*?)}-->#' , '// hook \\1' , $s); $s = preg_replace_callback('#//\s*hook\s+(\S+)#is' , 'plugin_compile_srcfile_callback' , $s); } else { break ; } } return $s; } function plugin_find_overwrite ($srcfile) { $plugin_paths = plugin_paths_enabled(); $len = strlen(APP_PATH); $returnfile = $srcfile; $maxrank = 0 ; foreach ($plugin_paths as $path=>$pconf) { $dir = file_name($path); $filepath_half = substr($srcfile, $len); $overwrite_file = APP_PATH."plugin/$dir/overwrite/$filepath_half" ; if (is_file($overwrite_file)) { $rank = isset ($pconf['overwrites_rank' ][$filepath_half]) ? $pconf['overwrites_rank' ][$filepath_half] : 0 ; if ($rank >= $maxrank) { $returnfile = $overwrite_file; $maxrank = $rank; } } } return $returnfile; } function plugin_paths_enabled () { static $return_paths; if (empty ($return_paths)) { $return_paths = array (); $plugin_paths = glob(APP_PATH.'plugin/*' , GLOB_ONLYDIR); if (empty ($plugin_paths)) return array (); foreach ($plugin_paths as $path) { $conffile = $path."/conf.json" ; if (!is_file($conffile)) continue ; $pconf = xn_json_decode(file_get_contents($conffile)); if (empty ($pconf)) continue ; if (empty ($pconf['enable' ]) || empty ($pconf['installed' ])) continue ; $return_paths[$path] = $pconf; } } return $return_paths; } function plugin_compile_srcfile_callback ($m) { static $hooks; if (empty ($hooks)) { $hooks = array (); $plugin_paths = plugin_paths_enabled(); foreach ($plugin_paths as $path=>$pconf) { $dir = file_name($path); $hookpaths = glob(APP_PATH."plugin/$dir/hook/*.*" ); if (is_array($hookpaths)) { foreach ($hookpaths as $hookpath) { $hookname = file_name($hookpath); $rank = isset ($pconf['hooks_rank' ]["$hookname" ]) ? $pconf['hooks_rank' ]["$hookname" ] : 0 ; $hooks[$hookname][] = array ('hookpath' =>$hookpath, 'rank' =>$rank); } } } foreach ($hooks as $hookname=>$arrlist) { $arrlist = arrlist_multisort($arrlist, 'rank' , FALSE ); $hooks[$hookname] = arrlist_values($arrlist, 'hookpath' ); } } $s = '' ; $hookname = $m[1 ]; if (!empty ($hooks[$hookname])) { $fileext = file_ext($hookname); foreach ($hooks[$hookname] as $path) { $t = file_get_contents($path); if ($fileext == 'php' && preg_match('#^\s*<\?php\s+exit;#is' , $t)) { $t = preg_replace('#^\s*<\?php\s*exit;(.*?)(?:\?>)?\s*$#is' , '\\1' , $t); } $s .= $t; } } return $s; }
_include:将srcfile文件编译等操作后存入tmp_path中,
plugin_compile_srcfile:编译源文件,寻找overwrite和hook的位置并合成最终文件
plugin_find_overwrite:寻找overwrite,只返回一个权重最高的文件名
plugin_compile_srcfile_callback:hook是在这个回调函数中替换的
glob — 寻找与模式匹配的文件路径
array_multisort — 对多个数组或多维数组进行排序
array_values — 返回数组中所有的值
plugin_paths_enabled:可用的插件,是根据插件下的conf.json中判别的
model.inc.php model.inc.php这个文件中定义了待会要载入的model等许多
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 if (DEBUG) { foreach ($include_model_files as $model_files) { include _include($model_files); } } else { $model_min_file = $conf['tmp_path' ].'model.min.php' ; $isfile = is_file($model_min_file); if (!$isfile) { $s = '' ; foreach ($include_model_files as $model_files) { $t = file_get_contents(_include($model_files)); $t = trim($t); $t = ltrim($t, '<?php' ); $t = rtrim($t, '?>' ); $s .= "<?php\r\n" .$t."\r\n?>" ; } $r = file_put_contents($model_min_file, $s); unset ($s); } include $model_min_file; }
根据debug来判断是否要用压缩版的model.min.php,一般线上模式都是用min版,响应更快
这边主要是遍历$include_model_files这个数组,将各个model去_include编译放置到临时目录下,然后trim下。然后把所有的都编译到model.min.php中
这里注意下,我开始一直有个疑问,那如果加入安装插件,按照这个代码逻辑如果临时目录下存在该已经编译过的文件了,那怎么插件会安装完就能用呢?
这个流程我反复看了好久,后来想到,肯定是安装的时候动手脚了把,肯定显式执行删除了。。果然翻看后看到了删除编译文件的操作
1 2 3 4 5 6 function plugin_clear_tmp_dir () { global $conf; rmdir_recusive($conf['tmp_path' ], TRUE ); xn_unlink($conf['tmp_path' ].'model.min.php' ); }
至于如果是自己开发插件,把debug改成2就可以了,这样每次都会自动去查找发现overwrite和hook。
index.inc.php 这个是index.php中最后include的一个了,之前那些都是初始化和引入文件。都是为了后面做准备,这个文件中应该会具体涉及到具体逻辑和路由转发功能了,否则怎么访问到页面对吧?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 <?php !defined('DEBUG' ) AND exit ('Access Denied.' ); $sid = sess_start(); $_SERVER['lang' ] = $lang = include _include(APP_PATH."lang/$conf[lang]/bbs.php" ); $grouplist = group_list_cache(); $uid = intval(_SESSION('uid' )); empty ($uid) AND $uid = user_token_get() AND $_SESSION['uid' ] = $uid;$user = user_read($uid); $gid = empty ($user) ? 0 : intval($user['gid' ]); $group = isset ($grouplist[$gid]) ? $grouplist[$gid] : $grouplist[0 ]; $fid = 0 ; $forumlist = forum_list_cache(); $forumlist_show = forum_list_access_filter($forumlist, $gid); $forumarr = arrlist_key_values($forumlist_show, 'fid' , 'name' ); $header = array ( 'title' =>$conf['sitename' ], 'mobile_title' =>'' , 'mobile_link' =>'./' , 'keywords' =>'' , 'description' =>strip_tags($conf['sitebrief' ]), 'navs' =>array (), ); $runtime = runtime_init(); check_runlevel(); $route = param(0 , 'index' ); if (!defined('SKIP_ROUTE' )) { switch ($route) { case 'index' : include _include(APP_PATH.'route/index.php' ); break ; case 'thread' : include _include(APP_PATH.'route/thread.php' ); break ; case 'forum' : include _include(APP_PATH.'route/forum.php' ); break ; case 'user' : include _include(APP_PATH.'route/user.php' ); break ; case 'my' : include _include(APP_PATH.'route/my.php' ); break ; case 'attach' : include _include(APP_PATH.'route/attach.php' ); break ; case 'post' : include _include(APP_PATH.'route/post.php' ); break ; case 'mod' : include _include(APP_PATH.'route/mod.php' ); break ; case 'browser' : include _include(APP_PATH.'route/browser.php' ); break ; default : include _include(APP_PATH.'route/index.php' ); break ; } } ?>
很多操作代码中的注释写的很清楚了,大赞作者!!
路由、Controller 层 index.inc.php最后的就是路由控制了,默认是route/index.php了,$route是由param这个xiunophp中的封装函数解析后得来的。
然后要注意的是这里也是执行_include的,
1 include _include(APP_PATH.'route/index.php' ); break ;
会先判断是否在tmp目录下存在这个文件,如果不存在就重新编译一份。
在编译的时候会找到所有的overwrite和hook并按照rank排列好,overwrite是按照权重找最高的,hook按照rank,然后最终合成为一个文件放在tmp这个临时文件中去了,
然后我们回到正题,这个route的index.php干了什么
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 <?php !defined('DEBUG' ) AND exit ('Access Denied.' ); $page = param(1 , 1 ); $order = $conf['order_default' ]; $order != 'tid' AND $order = 'lastpid' ; $pagesize = $conf['pagesize' ]; $active = 'default' ; $thread_list_from_default = 1 ; if ($thread_list_from_default) { $fids = arrlist_values($forumlist_show, 'fid' ); $threads = arrlist_sum($forumlist_show, 'threads' ); $pagination = pagination(url("$route-{page}" ), $threads, $page, $pagesize); $threadlist = thread_find_by_fids($fids, $page, $pagesize, $order, $threads); } if ($order == $conf['order_default' ] && $page == 1 ) { $toplist3 = thread_top_find(0 ); $threadlist = $toplist3 + $threadlist; } thread_list_access_filter($threadlist, $gid); $header['title' ] = $conf['sitename' ]; $header['keywords' ] = '' ; $header['description' ] = $conf['sitebrief' ]; $_SESSION['fid' ] = 0 ; include _include(APP_PATH.'view/htm/index.htm' );?>
注释仍然写得很清楚,再大赞作者
最后包含了_include(APP_PATH.’view/htm/index.htm’)这个,这里就到了视图最后显示给用户的阶段了
视图view 接着这个示例来看index.htm
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 <?php include _include(APP_PATH.'view/htm/header.inc.htm' );?> <div class ="row" > <div class ="col-lg-9 main" > <div class ="card card-threadlist" > <div class ="card-header" > <ul class ="nav nav-tabs card-header-tabs" > <li class ="nav-item" > <a class ="nav-link <?php echo $active == 'default' ? 'active' : '';?>" href ="./<?php echo url(" $route ");?> "><?php echo lang('new_thread' );?> </a > </li > </ul > </div > <div class ="card-body" > <ul class ="list-unstyled threadlist mb-0" > <?php include _include(APP_PATH.'view/htm/thread_list.inc.htm' );?> </ul > </div > </div > <?php include _include(APP_PATH.'view/htm/thread_list_mod.inc.htm' );?> <nav class ="my-3" > <ul class ="pagination justify-content-center flex-wrap" > <?php echo $pagination; ?> </ul > </nav > </div > <div class ="col-lg-3 d-none d-lg-block aside" > <a role ="button" class ="btn btn-primary btn-block mb-3" href ="<?php echo url('thread-create-'.$fid);?>" > <?php echo lang('thread_create_new' );?> </a > <div class ="card card-site-info" > <div class ="m-3" > <h5 class ="text-center" > <?php echo $conf['sitename' ];?> </h5 > <div class ="small line-height-3" > <?php echo $conf['sitebrief' ];?> </div > </div > <div class ="card-footer p-2" > <table class ="w-100 small" > <tr align ="center" > <td > <span class ="text-muted" > <?php echo lang('threads' );?> </span > <br > <b > <?php echo $runtime['threads' ];?> </b > </td > <td > <span class ="text-muted" > <?php echo lang('posts' );?> </span > <br > <b > <?php echo $runtime['posts' ];?> </b > </td > <td > <span class ="text-muted" > <?php echo lang('users' );?> </span > <br > <b > <?php echo $runtime['users' ];?> </b > </td > <?php if ($runtime['onlines' ] > 0 ) { ?> <td > <span class ="text-muted" > <?php echo lang('online' );?> </span > <br > <b > <?php echo $runtime['onlines' ];?> </b > </td > <?php } ?> </tr > </table > </div > </div > </div > </div > <?php include _include(APP_PATH.'view/htm/footer.inc.htm' );?> <script > $('li[data-active="fid-0"]').addClass('active'); </script >
在上个部分路由控制层最后包含这个文件又执行了_include这个函数,所以这其中的hook一样到最后都是会被插件中对应的hook内容增加上去。
而且这个文件中还包含了header.inc.htm和footer.inc.htm内容,所以_include会嵌套包含最终组合编译成一个文件放在tmp的临时目录中,等到下次访问就直接会访问那个目录下的文件,速度就会提高很多了!!
然后将内容返回给前端浏览器渲染给我们了。
收获 暂时先简单记录下这个流程,没有给出太多的细节,因为看代码更直观。