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(框架的主要目录),插件目录。 代码:
    //跟zendframework  很象
    @set_include_path(get_include_path() . PATH_SEPARATOR .
    载入API支持(var目录下), require_once ‘Typecho/Common.php’;
    然后调用程序初始化。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());