typecho 源码解读
typecho 源码解读
typecho没有用到命名空间。此框架用到较多以类封装的静态函数的方式。解读这个框架的源码,首先,了解其大致的加载流程。其次呢,了解一些重要的类的加载,比如数据库的加载。再次,就是根据实际操作,查看请求的具体信息,来了解一个请求,是如何加载进来的。还有,主题里面的各个布局文件,是如何正确的加载进来。整个框架,一些重要的变量是如何传递的?
通过这个框架的研究,加上之前多媒体后台,前后端不分离的项目,php、js、html混合在一起,开发的难度确实大。如果选择的框架又不好用或者对代码没有一定的约束力,那么整体代码都失控了。
源码解读
入口文件index.php
首先,检测 __TYPECHO_ROOT_DIR__这个变量(全部文件都以此变量区分是否属于入口加载,)加载config.inc.php,判断结构 if( ! a && !b) 等价与 if(!(a || b)) ,即a,b只要有一个为真,就不执行判断内部的逻辑,也即__TYPECHO_ROOT_DIR__定义或者成功加载 config.inc.php,就不会进入if内部。但实际变量不存在,那么就看能不能加载到配置文件,config.inc.php,如果没有加载成功,则进重定向到安装模块,进行安装。
正常加载到配置文件,那么执行后面四个语言:
/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');
/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();
/** 开始路由分发 */
Typecho_Router::dispatch();
/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();
那么就需要看看配置文件到底是啥?怎么能为上面四句提供环境的?
安装模块install.php
此文件代码较多,但是php代码200行,其他都是html。目的,如果没有定义config,那么正常安装。如果有config,检查发现有数据库,数据库中有install等字段,那么直接屏蔽用户的请求,给个404。
主要的逻辑是:定义根目录,设置系统加载的路径,定义根目录,(全局变量的形式: __TYPECHO_ROOT_DIR__),
- 根目录、
- 插件目录/usr/plugins
- 模板目录/usr/themes
- 后台目录/admin/。
设置加载目录: /var(框架的主要目录),插件目录。 代码:
载入API支持(var目录下), require_once ‘Typecho/Common.php’;//跟zendframework 很象 @set_include_path(get_include_path() . PATH_SEPARATOR .
然后调用程序初始化。Typecho_Common::init();
以上是不存在配置文件的执行方式,如果存在则加载执行下面的代码。
获取数据库对象,$db = Typecho_Db::get();
根据table.options表中installed字段,如果$installed为空值或者有值value=1都会进行404响应Typecho_Response::setStatus(404);,并退出。
正常往下执行的时候,有一端根据referer来阻止访问的代码:
// 挡掉可能的跨站请求
if (!empty($_GET) || !empty($_POST)) {
if (empty($_SERVER['HTTP_REFERER'])) {
exit;
}
$parts = parse_url($_SERVER['HTTP_REFERER']);
if (!empty($parts['port'])) {
$parts['host'] = "{$parts['host']}:{$parts['port']}";
}
if (empty($parts['host']) || $_SERVER['HTTP_HOST'] != $parts['host']) {
exit;
}
}
后面就是定义一些功能函数,然后进入到安装界面。代码分析不够透彻,先略过。
配置文件config.inc.php
配置文件是安装之后,产生的,跟上面安装文件,流程大致一样。程序的Typecho_Common::init();具体是什么作用?需要研究。
配置文件的代码:
/** 定义根目录 */
define('__TYPECHO_ROOT_DIR__', dirname(__FILE__));
/** 定义插件目录(相对路径) */
define('__TYPECHO_PLUGIN_DIR__', '/usr/plugins');
/** 定义模板目录(相对路径) */
define('__TYPECHO_THEME_DIR__', '/usr/themes');
/** 后台路径(相对路径) */
define('__TYPECHO_ADMIN_DIR__', '/admin/');
/** 设置包含路径 */
@set_include_path(get_include_path() . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . '/var' . PATH_SEPARATOR .
__TYPECHO_ROOT_DIR__ . __TYPECHO_PLUGIN_DIR__);
/** 载入API支持 */
require_once 'Typecho/Common.php';
/** 程序初始化 */
Typecho_Common::init();
/** 定义数据库参数 */
$db = new Typecho_Db('Pdo_SQLite', 'typecho_');
$db->addServer(array (
'file' => 'D:/phpStudy/PHPTutorial/zend/typecho/usr/5cc69e9d3bb13.db',
), Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
那么剩下的就是,看index.php的流程了。主要分为:
- 程序初始化Typecho_Common::init();
- 初始化组件Typecho_Widget::widget(‘Widget_Init’);
- 注册一个初始化插件Typecho_Plugin::factory(‘index.php’)->begin();
- 开始路由分发Typecho_Router::dispatch();
- 注册一个结束插件Typecho_Plugin::factory(‘index.php’)->end();
程序初始化
Typecho_Common是一个类文件,类之前定义了一些函数。总代码比较多。init代码如下:
此代码的作用是,提供了一个自动加载类的方式。对比Zend、Thinkphp5.1、php-crud-api中加载的方式。同样也是注册一个加载函数,spl_autoload_register,另外,加上前面的自动加载目录,所以能自动加载到类。
/**
* 自动载入类
*
* @param $className
*/
public static function __autoLoad($className)
{
@include_once str_replace(array('\\', '_'), '/', $className) . '.php';
}
/**
* 程序初始化方法
*
* @access public
* @return void
*/
public static function init()
{
/** 设置自动载入函数 */
spl_autoload_register(array('Typecho_Common', '__autoLoad'));
/** 兼容php6 */
if (function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc()) {
$_GET = self::stripslashesDeep($_GET);
$_POST = self::stripslashesDeep($_POST);
$_COOKIE = self::stripslashesDeep($_COOKIE);
reset($_GET);
reset($_POST);
reset($_COOKIE);
}
/** 设置异常截获函数 */
set_exception_handler(array('Typecho_Common', 'exceptionHandle'));
}
初始化组件
Typecho_Widget::widget(‘Widget_Init’);
这代码Typecho_Widget::widget的大致所有是:静态化一个实例,并储存到主件池$_widgetPool变量中(私有静态变量)。如果这个类需要有多个实例的话,那么可以起一个别名,以@符号进行分隔,如Widget_Init@first,那么如果要获得这个实例,alias(‘Widget_Init’,’first’)。除此之外,还为组件自动绑定变量(跟传入的参数差不多),这个功能有点象容器注入(控制反转)。
此函数的注释如下:
/**
* 工厂方法,将类静态化放置到列表中
*
* @access public
* @param string $alias 组件别名
* @param mixed $params 传递的参数
* @param mixed $request 前端参数
* @param boolean $enableResponse 是否允许http回执
* @return Typecho_Widget
* @throws Typecho_Exception
*/
public static function widget($alias, $params = NULL, $request = NULL, $enableResponse = true)
{
//$className 跟$alias 以@分隔,否则,两者都是$alias
$parts = explode('@', $alias);
$className = $parts[0];
$alias = empty($parts[1]) ? $className : $parts[1];
if (isset(self::$_widgetAlias[$className])) {
$className = self::$_widgetAlias[$className];
}
//1、从组件池中获取,如果不存在,那么先创建一个放到池子内。
if (!isset(self::$_widgetPool[$alias])) {
/** 如果类不存在 */
if (!class_exists($className)) {
throw new Typecho_Widget_Exception($className);
}
/** 初始化request */
if (!empty($request)) {
$requestObject = new Typecho_Request();
$requestObject->setParams($request);
} else {
$requestObject = Typecho_Request::getInstance();
}
/** 初始化response */
$responseObject = $enableResponse ? Typecho_Response::getInstance()
: Typecho_Widget_Helper_Empty::getInstance();
/** 初始化组件 */
$widget = new $className($requestObject, $responseObject, $params);//注入了一些参数,有点象容器
$widget->execute();
self::$_widgetPool[$alias] = $widget;
}
//2、直接从组件池中获取
return self::$_widgetPool[$alias];
}
从实际的请求中,发现manage-pages.php其实已经是一个独立的页面,那么此页面是如何完成页面渲染的呢?通过导入include ‘common.php’;完成页面的初始化工作。然后,发现下面的代码,作用是,将值分配到$pages对象中,然后遍历得到页面的结果。
Typecho_Widget::widget('Widget_Contents_Page_Admin')->to($pages)
上面的Request、Response都还未深入分析。那么,下一步:
注册一个初始化插件
Typecho_Plugin::factory(‘index.php’)->begin();插件代码功能大概有400行。
功能也是将插件进行了缓存。(缓存?是不是类似Zend_Register?也能集中管理?)
代码如下:
/**
* 获取实例化插件对象
*
* @access public
* @param string $handle 插件
* @return Typecho_Plugin
*/
public static function factory($handle)
{
return isset(self::$_instances[$handle]) ? self::$_instances[$handle] :
(self::$_instances[$handle] = new Typecho_Plugin($handle));
}
//Typecho_Plugin的构造函数
/**
* 插件初始化
*
* @access public
* @param string $handle 插件
*/
public function __construct($handle)
{
/** 初始化变量 */
$this->_handle = $handle;
}
从上面的代码来看,注册一个插件,并没有做什么实质的动作。估计是在其他地方,方便调用。类似注册成全局的变量一个。(暂时这样猜测)。那么到下一步。
开始路由分发
Typecho_Router::dispatch();
这个类的代码两百多行。
代码如下:
/**
* 路由分发函数
*
* @return void
* @throws Exception
*/
public static function dispatch()
{
/** 获取PATHINFO */
$pathInfo = self::getPathInfo();
foreach (self::$_routingTable as $key => $route) {
if (preg_match($route['regx'], $pathInfo, $matches)) {
self::$current = $key;//当前匹配到的路由表
try {
/** 载入参数 */
$params = NULL;
if (!empty($route['params'])) {
unset($matches[0]);
$params = array_combine($route['params'], $matches);
}
//同样,将当前匹配到的$route['widget'],注册成一个组件。
$widget = Typecho_Widget::widget($route['widget'], NULL, $params);
//如果当前路由有action属性,那么调用此方法
if (isset($route['action'])) {
$widget->{$route['action']}();
}
//下面的,是如何获取到数据的??
Typecho_Response::callback();
return;
} catch (Exception $e) {
if (404 == $e->getCode()) {
Typecho_Widget::destory($route['widget']);
continue;
}
throw $e;
}
}
}
/** 载入路由异常支持 */
throw new Typecho_Router_Exception("Path '{$pathInfo}' not found", 404);
}
那么在顺道看有一下Typecho_Response::callback()这个函数是做哪些东西?
代码如下:
/**
* 结束前的统一回调函数
*/
public static function callback()
{
//注意此变量为静态变量,相当于锁功能。即在一次请求中,后面的代码只执行一次。
static $called;
if ($called) {
return;
}
$called = true;
foreach (self::$_callbacks as $callback) {
call_user_func($callback);
}
}
//Typecho_Response类并不是真正的单例模式,因为其__construct、__clone没有做任何处理,也未定义。
/**
* 获取单例句柄
*
* @access public
* @return Typecho_Response
*/
public static function getInstance()
{
if (null === self::$_instance) {
self::$_instance = new Typecho_Response();
}
return self::$_instance;
}
另外,Typecho_Response类还有中写方法throwXml、throwJson、redirect、goBack
注册一个结束插件
Typecho_Plugin::factory(‘index.php’)->end();
有点迷糊了,Typecho_Plugin::factory(‘index.php’)返回的就是一个Typecho_Plugin,参数只是一个区分而已。而插件并没有begin、end方法。这是为什么呢?难道有更深的机制?
这个到底是什么?为什么能调用这样了两个方法begin、start?
类反射,查看一下,也没有什么想要的信息。
$tmp=Typecho_Plugin::factory('index.php');
var_dump(get_class_methods($tmp));
测试过了,如果注释掉注册插件这两行代码,其实对页面也没有任何影响。另外,随便写一个方法Typecho_Plugin::factory(‘index.php’)->ttt(),也不报错。这是因为使用了__call魔术方法。另外还使用了__set,__get魔术方法。如果将此Typecho_Plugin::__call方法注释掉,错误提示如下:
//下面这个是框架的报错提示,非系统提醒
Call to undefined method Typecho_Plugin::begin();
Typecho_Plugin::__call源代码(实现调用任意方法):
/**
* 回调处理函数
*
* @access public
* @param string $component 当前组件
* @param string $args 参数
* @return mixed
*/
public function __call($component, $args)
{
$component = $this->_handle . ':' . $component;
$last = count($args);
$args[$last] = $last > 0 ? $args[0] : false;
if (isset(self::$_plugins['handles'][$component])) {
$args[$last] = NULL;
$this->_signal = true;
foreach (self::$_plugins['handles'][$component] as $callback) {
$args[$last] = call_user_func_array($callback, $args);
}
}
return $args[$last];
}
那么到了这一步,自然想知道,此框架的插件机制是如何的?
官网给的helloworld例子,结合本地的helloworld插件/usr/plugins/HelloWorld的代码,
//
class HelloWorld_Plugin implements Typecho_Plugin_Interface
{
/**
* 激活插件方法,如果激活失败,直接抛出异常
*
* @access public
* @return void
* @throws Typecho_Plugin_Exception
*/
public static function activate()
{
//这其实就是设置了一个回调函数。navBar确实在admin/menu.php中调用了。
Typecho_Plugin::factory('admin/menu.php')->navBar = array('HelloWorld_Plugin', 'render');
}
- activate: 插件的激活接口,主要填写一些插件的初始化程序。
- deactivate: 这个是插件的禁用接口,主要就是插件在禁用时对一些资源的释放。
- config: 插件的配置面板,用于制作插件的标准配置菜单。
- personalConfig: 个人用户的配置面板,基本用不到。
- render: 自己定义的方法,用来实现插件要完成的功能。
admin/menu.php
在此处调用:navBar(); ?>
<?php if(!defined('__TYPECHO_ADMIN__')) exit; ?>
<div class="typecho-head-nav clearfix" role="navigation">
<nav id="typecho-nav-list">
<?php $menu->output(); ?>
</nav>
<div class="operate">
<?php Typecho_Plugin::factory('admin/menu.php')->navBar(); ?><a title="<?php
if ($user->logged > 0) {
$logged = new Typecho_Date($user->logged);
_e('最后登录: %s', $logged->word());
}
?>" href="<?php $options->adminUrl('profile.php'); ?>" class="author"><?php $user->screenName(); ?></a><a class="exit" href="<?php $options->logoutUrl(); ?>"><?php _e('登出'); ?></a><a href="<?php $options->siteUrl(); ?>"><?php _e('网站'); ?></a>
</div>
</div>
所以呢,结合此插件,大致明白了注册开始、结束插件的意思呢,反正就是预留了一些代码钩子,如果用户确实实现了这个插件,那么就调用了。
理解这种插件机制的作用?为啥这样设计?
个人理解,其实这样设计并没有什么太大的好处。插件嘛,类似于AOP,在固定的位置调用方法。其实也能理解js的事件回调。在固定的位置触发一个事件,然后响应这个事件。测试过,确实,一个地方,触发了这个插件,多个插件都响应了。插件代码中的注释,居然还能作为内容显示在页面中。
页面流转
发现这个框架的页面跳转,其实跟之前想得很不一样。首先,管理后台,是要跳到/admin/文件夹中,然后就不再是统一的入口呢。
/admin文件夹中,有两个文件,common-js.php跟common.php。估计前面的是为了加载js用的,后面,相当于一个入口函数。
common-js.php
php往js脚本中注入了一些变量。js脚本功能:处理消息机制(立即执行函数形式,防止变量泄漏成全局)、导航菜单 tab 聚焦时展开下拉菜单。加载js库jquery.js、jquery-ui.js、typecho.js。
common.php
数据库操作
定义数据库
/** 定义数据库参数 */
$db = new Typecho_Db('Pdo_SQLite', 'typecho_');
$db->addServer(array (
'file' => 'D:/phpStudy/PHPTutorial/zend/typecho/usr/5cc69e9d3bb13.db',
), Typecho_Db::READ | Typecho_Db::WRITE);
Typecho_Db::set($db);
获取数据库对象实例:
/** 初始化数据库 */
$this->db = Typecho_Db::get();
知识
PHP获取当前类名、方法名
__CLASS__ 获取当前类名
__FUNCTION__ 当前函数名(confirm)
__METHOD__ 当前方法名 (bankcard::confirm)
__FUNCTION__ 函数名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该函数被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__CLASS__ 类的名称(PHP 4.3.0 新加)。自 PHP 5 起本常量返回该类被定义时的名字(区分大小写)。在 PHP 4 中该值总是小写字母的。
__METHOD__ 类的方法名(PHP 5.0.0 新加)。返回该方法被定义时的名字(区分大小写)。
get_class(class name);//取得当前语句所在类的类名
get_class_methods(class name);//取得class name 类的所有的方法名,并且组成一个数组
get_class_vars(class name);//取得class name 类的所有的变亮名,并组成一个数组
反射的形式获取:
//用反射类可以获得私有属性和私有方法
$ref = new ReflectionClass(Class1);//Class1 可以为对象实例 $class = new Class();
print_r($ref->getMethods());
print_r($ref->getProperties());