0%

PHPCMS 源码分析

最近在做一些和 PHPCMS 相关的二次开发,记录一下收获。

目录结构

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

| -- api 接口文件目录
| -- caches 缓存文件目录
| -- configs 系统配置文件目录 (配置目录放在缓存目录里感觉不太合理)
| -- cache_* 缓存相关目录

| -- phpcms phpcms 框架主目录
| -- languages 框架语言包目录
| -- libs 框架主类库、主函数库目录
| -- model 框架数据库模型目录
| -- modules 框架模块目录
| -- templates 框架系统模板目录
| -- phpsso_server phpsso 主目录

| -- statics 静态资源相关
| -- css css 目录
| -- images 图片目录
| -- js js 目录

| -- uploadfile 网站附件目录
| -- admin.php 后台登陆入口
| -- index.php 程序主入口
| -- crossdomain.xml flash 跨域传输文件
| -- robots.txt 搜索引擎蜘蛛限制配置文件
| -- favicon.ico 系统 icon 图标
| -- js.html JS站群跨域
| -- plugin.php 插件相关
| -- api.php 外部 api 调用地址

生命周期

index.php 入口文件

1
2
3
4
5
6

define('PHPCMS_PATH', dirname(__FILE__).DIRECTORY_SEPARATOR);

include PHPCMS_PATH.'/phpcms/base.php';

pc_base::creat_app();

我们看到入口文件定义了一个常量 PHPCMS_PATH 为当前路径, 同时加载 phpcms 目录下的 base.php 文件,随后又调用 pc_base::create_app()。 非常类似 Yii,创建一个运行的 app,也是常见的一种入口文件模式。

base.php 核心文件

接下来我们来看一下比较重要的一个文件 base.php, 位于 phpcms/base.php

文件阅读主要分为两个部分

第一部分定义了一堆的路径常量,这在框架的初始化过程中也很常见。
剩下的主要是一个 pc_base 的类定义,这个是我们重点要关注的类。

先来看一下第一部分,常量定义。

首先第一行我们注意到 define('IN_PHPCMS', true); 定义了一个 IN_PHPCMS 的常量,其实目的是确保从入口文件进入的,你会在模块中经常见到如下定义:

defined('IN_PHPCMS') or exit('No permission resources.');

如果我们通过 URL 直接访问这个目录下的文件,由于没有定义 IN_PHPCMS 常量,便无法访问。在一些老的代码里我们经常会看到这样的用法,比如 CI、Discuz。其实我们采用程序与入口分离即可,把入口文件以及静态资源单独放一个 www 作为根目录,而把框架核心放到一个其他目录,分配好权限即可。也不用每个文件头部这么蛋疼的写这么一句了。

接着往下看,就是一些路径,时间,字符集等的一些常量设定了。

注意到如下这句:

1
2
//输出页面字符集
header('Content-type: text/html; charset='.CHARSET);

这里默认输出的就是 text/html 类型。 不过5.4 以上就不用手动写这句了,会默认发送 utf-8。

期间会调用 pc_base 的一些静态方法。比如

1
2
3
4
5
6
//加载公用函数库
pc_base::load_sys_func('global');
pc_base::load_sys_func('extention');
pc_base::auto_load_func();
//加载配置
pc_base::load_config('system','errorlog')

我们来一个一个整理。

pc_base::load_sys_func

pc_base::load_sys_func 这个方法主要用于加载系统函数, 位于 phpcms/libs/functions 下。
命名规则是 xx.func.php

load_sys_func 实际上是一个代理方法,本质调用的是 pc_base 类的 _load_func私有方法,会使用静态变量数组来缓存结果,key 则是 md5 过后的 path 值。

pc_base::auto_load_func

我们在来看一下 pc_base::auto_load_func 看名字像是会自动加载一些函数类库,类似 composer 中的 classmap, 看下具体实现

1
2
3
4
5
6
7
8
9
10
private static function _auto_load_func($path = '') {
if (empty($path)) $path = 'libs'.DIRECTORY_SEPARATOR.'functions'.DIRECTORY_SEPARATOR.'autoload';
$path .= DIRECTORY_SEPARATOR.'*.func.php';
$auto_funcs = glob(PC_PATH.DIRECTORY_SEPARATOR.$path);
if(!empty($auto_funcs) && is_array($auto_funcs)) {
foreach($auto_funcs as $func_path) {
include $func_path;
}
}
}

原来是会自动加载 phpcms/libs/functions/autoload 目录下的所有类,使用 glob 函数获取所有文件后遍历加载。

至此,我们知道一开始会载入一个全局的函数库(global.func.php)和一个自定义的函数库(extension.func.php),然后自动加载 autoload 目录下的类库。
加载公用函数库部分结束。

pc_base::load_config

我们接着看一下加载配置文件的函数 pc_base::load_config('文件','key') , 我们查找一下 caches/configs/system.php 发现这个文件中返回的就是一个二维数组。在很多框架中,config 的配置都是类似的实现,比如 laravel 中 Config::get('database.default') 就代表查找配置目录中 database.php 文件中的 default 对应的配置。所以 pc_base::load_config('system', 'errorlog') 的意思就是获取 caches/configs/system.php 中的 errorlog 键值。

来看一下 pc_base::load_config 的实现, 也是用一个静态变量数组来存储。

逻辑大概如下:

  1. 如果静态变量中已经有对应的 key 值,直接返回。
  2. 设定读取的配置目录 位于 caches/configs 下。
  3. 如果没有指定 key ,那么会返回整个文件数组。
  4. 可以指定额外的 default 值, 当获取的 key 对应的值不存在的时候会返回 default 值。
  5. 把对应的键值写入到静态变量中方便下一次调用。

纵观几个方法,无一不是通过这种静态变量的形式缓存的,同时也把载入路径写死在函数中,或者通过参数传递,已经不是 DRY 的做法了。这可能是具有一定代表意义的时代性的写法,也影响了不少人,可以从当时流行的其他代码中窥见。

其实在现在来看,PHP 5.3 以后带来的命名空间特性,以及 composer 的流行,已经不需要再这样手动载入了,这些方法本质都只是载入对应的类库文件,composer 完全可以满足,而只需要一行 require 'vendor/autoload.php';

我们总结一下 pc_base 的一些常见静态方法

pc_base::load_sys_class 加载系统类,位于 phpcms/libs/classes
pc_base::load_app_class 加载模块中的类,位于 modules/模块目录/classes
pc_base::load_model 加载模型类,位于 phpcms/model
pc_base::load_sys_func 加载系统函数,位于 phpcms/libs/functions
pc_base::auto_load_func 自动载入系统函数库,位于 phpcms/libs/functions/autoload
pc_base::load_app_func 加载模块函数,位于 modules/模块目录/functions
pc_base::load_config 加载配置文件,位于 caches/configs/xxx.php

pc_base::create_app

我们回到 index.php 的最后一行的方法 pc_base::create_app(),这也是刚刚没有分析的一个方法,我们来看一下定义。

1
2
3
4
5
6
/**
* 初始化应用程序
*/
public static function creat_app() {
return self::load_sys_class('application');
}

可以看到本质是调用 load_sys_class 方法,通过前面的分析我们知道其实是会去加载 phpcms/libs/classes/application.class.php 文件,这里在提一点,如果是载入类,支持使用 MY_xxxx 的形式来扩展自定义的类。关键部分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
if (file_exists(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
include PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php';
$name = $classname;
//my_path 方法会判断是否存在一个以 MY_ 开头的同名类文件
if ($my_path = self::my_path(PC_PATH.$path.DIRECTORY_SEPARATOR.$classname.'.class.php')) {
include $my_path;
$name = 'MY_'.$classname;
}
if ($initialize) {
$classes[$key] = new $name;
} else {
$classes[$key] = true;
}
return $classes[$key];
} else {
return false;
}

假设我们访问的是 modules/content/index.php ,这也是默认的首页路由,我们可以通过创建一个 MY_index.php 来覆盖原有的 index.php ,这也是 PHPCMS 的一种扩展机制。

因此,pc_base::create_app() 本质就是实例化 phpcms/libs/classes/application.class.php

application.class.php

现在我们来看一下 application.class.php

1
2
3
4
5
6
7
8
9
10
/**
* 构造函数
*/
public function __construct() {
$param = pc_base::load_sys_class('param');
define('ROUTE_M', $param->route_m());
define('ROUTE_C', $param->route_c());
define('ROUTE_A', $param->route_a());
$this->init();
}

实例化的时候注入了一个 param 类 ,猜想是解析地址成 c、m、a 之类的,然后调用 init 初始化。

params.class.php

我们来看一下 params

比较关键的几个地方:

1
2

$this->route_config = pc_base::load_config('route', SITE_URL) ? pc_base::load_config('route', SITE_URL) : pc_base::load_config('route', 'default');

主要是根据当前的主机地址来获取 route_config 如果没有的话采用 default 默认路由配置。
配置文件位于 caches/config/route.php

1
2
3
return array(
'default'=>array('m'=>'content', 'c'=>'index', 'a'=>'init'),
);

这也就意味着默认情况下会访问 modules/content/index.php 的 init 方法。

而route_* 系列方法则会把当前 URL 参数的 m、c、a 通过安全处理后返回对应的值。

我们回到 application.class.php 的构造函数,可以看到通过调用 route_* 系列,可以把对应的module,controller 和 action 绑定到 ROUTE_* 常量中。

init

终于到了最后的 init 方法了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 调用件事
*/
private function init() {
$controller = $this->load_controller();
if (method_exists($controller, ROUTE_A)) {
if (preg_match('/^[_]/i', ROUTE_A)) {
exit('You are visiting the action is to protect the private action');
} else {
call_user_func(array($controller, ROUTE_A));
}
} else {
exit('Action does not exist.');
}
}

首先吐槽下,调用件事 这个注释,这不是我写错了,而是源码中确实存在的,也是醉了,原谅我语死早。

第一行调用 load_controller 获取控制器实例,这个控制器位于一个具体模块下也就是 m=? 中 m 对应的值,同时也支持 MY_xx 的形式覆盖。注意到这里会判断以_开头的方法,会认为是受保护的方法,不允许访问,命名的时候需要注意。

最后调用 call_user_func() 至此框架流程就算是结束了。是一个比较典型的传统 MVC 的框架形式,而现代化的那些框架则抽象了 HTTP 的整个流程,比如 symfonyhttp component,带来了更多的可能性以及扩展性,值得我们学习跟上脚步。

至于其他的重头戏都在 modules 目录中,由于和业务相关就不重点分析了。了解了这整一个流程,在做相关的开发相信就会更得心应手。

–EOF–

坚持原创分享,您的支持将鼓励我继续创作!