标签 source code 下的文章

上一节

这篇我们就讲讲首页的业务逻辑,首先我们看看路由。

[index] => Array
        (
            [url] => /
            [widget] => Widget_Archive
            [action] => render
            [regx] => |^[/]?$|
            [format] => /
            [params] => Array
                (
                )

        )

由路由表中的正则表达式我们能看出首页的URL为https://blog.phpgao.com/http://www.phpgao.com,并且不接受任何参数。

下面我们找到了Widget_Archive类,让我们先看看这个类的继承关系(其中Typecho_WidgetWidget_Abstract为抽象类):

inherit.png

由上一节我们可以得到结论,当路由表匹配到/,会实例化Widget_Archive类,他位于var/Widget/Archive.php,其方法为render。

下面我们一步步跟进,看看到底发生了些什么?

再深入之前,我们需要有一些基本的概念,在此需要讲清楚,便于理解typecho的设计模式:

  • 基类Typecho_Widget

该类位于var/Typecho/Widget.php,是var/Widget文件夹下所有类的基类,也就是说,几乎所有与业务有关的功能,就是继承此类。

纵观此类,方法不是很多,老高在此挑几个重要的讲解:


    /**
     * 获取对象插件句柄
     * 此方法为插件的实现提供了快捷的语法
     */
    public function pluginHandle($handle = NULL)
    {
        return Typecho_Plugin::factory(empty($handle) ? get_class($this) : $handle);
    }
    
    /**
     * 将类本身赋值
     * 模板中很常见,将自己复制给某一个变量,在调用next()方法循环输出
     */
    public function to(&$variable)
    {
        return $variable = $this;
    }
    
    /**
     * 将每一行的值压入堆栈
     * 把数据放到自己的'肚子'里
     */
    public function push(array $value)
    {
        //将行数据按顺序置位
        $this->row = $value;
        $this->length ++;

        $this->stack[] = $value;
        return $value;
    }
    
    
    /**
     * 返回堆栈每一行的值
     * 有点像从数据库中循环读取结果集
     * 与push相对应,值得大家参考
     */
    public function next()
    {
        if ($this->stack) {
            $this->row = @$this->stack[key($this->stack)];
            next($this->stack);
            $this->sequence ++;
        }

        if (!$this->row) {
            reset($this->stack);
            if ($this->stack) {
                $this->row = $this->stack[key($this->stack)];
            }
            
            $this->sequence = 0;
            return false;
        }

        return $this->row;
    }
    
    # 魔术方法,当调用一个不存在的方法时触发,输出当前$this->name的属性值。
    public function __call($name, $args)
    
    # 魔术方法,当获取不存在的属性时,首先会在$this->row中寻找,如果未找到,则调用___name()方法,并将结果返回,如果还是不存在,就以name为挂载点,触发插件事件,并返回结果。
    
    public function __get($name)
    public function __set($name, $value)

码字不易,转载请注明出处。

上一节

时光匆匆,我们终于来到路由工作的地方。几乎每个MVC框架都不会缺少路由功能,因为他实在太重要了。

路由的功能简单的说就是根据请求找到对应业务代码,请求来自外部,业务由系统提供,路由器为二者建立了可靠的链接。

typecho和thinkphp都有路由模块,虽然实现方式不同,但用法却惊人的相似!

thinkphp:

Dispatcher::dispatch();

typecho:

Typecho_Router::dispatch();

typecho的路由实现是保存在数据库中的路由表,使用正则匹配路径,这一点和Django框架有点像,thinkphp则是基于自己设定的一套规则,详见官方文档

typecho的路由表如下数组组成,路由器类会使用子数组中的regx正则式逐个匹配pathinfo中的路径,如果匹配成功,立即初始化并执行该类对应的action。

以index为例,如果使用正则表达式匹配成功,系统就会新建Widget_Archive类并执行其render方法,看他的名字就知道执行的是渲染页面的方法,也就是之前说的业务

Array
(
    [index] => Array
        (
            [url] => /
            [widget] => Widget_Archive
            [action] => render
            [regx] => |^[/]?$|
            [format] => /
            [params] => Array
                (
                )

        )

    [archive] => Array
        (
            [url] => /blog/
            [widget] => Widget_Archive
            [action] => render
            [regx] => |^/blog[/]?$|
            [format] => /blog/
            [params] => Array
                (
                )

        )
    # ....

有的同学可能会说,typecho的路由表写在数据库里,不够灵活。其实作者也考虑到了,大家可以自由扩展路由表。有兴趣的同学可以去看看Helper类中addRoute和removeRoute的用法。举个例子,如果你需要自定义一个URL,比如www.phpgao.com/robots.txt,就可以在路由表加入如下路由:

[robots] => Array
        (
            [url] => /robots.txt
            [widget] => Robots_Action
            [action] => render
        )

需要注意的是:

  • 新建路由时只需要url widget action 三个参数就够了
  • 如果url中包含参数,需要在url体现,具体实现请看var/Typecho/Router/Parser.php中的parse方法
  • 不需要担心widget方法找不到你的类,因为插件的安装路径已经在config.inc.php文件中加入了包含路径
  • 路由的参数和正则表达式系统自动生成
  • 在初始化阶段,Widget_Options类做了一个优化,具体在execute方法的最后,动作是将解析后的路由表保存在键值为0的数组里,以后初始化的时候先看看有没有缓存,这个缓存在路由删除或新建的时候会被删除,可参考var/Helper.php中的addRoute和removeRoute。

URL中如果有参数,以page为例,使用规则如下:

[page] => Array
        (
            [url] => /[slug].html
            [widget] => Widget_Archive
            [action] => render
        )

其中[]里的slug即参数名称,此处没有限定参数格式,默认为char,如果想要限定格式,就必须参照Typecho_Router_Parser类的_defaultRegx属性,即:

$this->_defaultRegx = array(
            'string' => '(.%s)',
            'char'   => '([^/]%s)',
            'digital'=> '([0-9]%s)',
            'alpha'  => '([_0-9a-zA-Z-]%s)',
            'alphaslash'  => '([_0-9a-zA-Z-/]%s)',
            'split'  => '((?:[^/]+/)%s[^/]+)',
        );

如果想限定参数为数字,那么[url] => /[slug].html需要改为[url] => /[slug:digital].html

如果需要精确限定次数,即4个数字,格式为[url] => /[slug:digital:4].html

如果需要限定次数在某个范围,即1-4个数字,格式为[url] => /[slug:digital:1:4].html

其他格式同理。

如果要某个插件要添加action而不需要绑定到某个URL怎么办?

如默认文章发布提交的地址为

/action/contents-post-edit?_=XXX

现在我想扩充这个地址,想要系统能够处理图片的发布怎么办?

/action/picture-post-edit?_=XXX

答:

我们可以注意到Helper::addAction方法,该方法接收两个参数,action名字和执行action使用的类名。

也就是说我们绑定某个action到插件的action.php文件的action()方法中去,当Widget_Do在处理从Dispatcher::dispatch()接收到的参数后,Widget_Do会在自己的MAP属性(系统默认)和数据库(插件添加)中找到合适的处理此请求的类。

找到这个类后,Widget_Do会使用PHP中ReflectionClass反射技术处理并调用目标类的action方法!

这样就完成了原先系统无法处理的请求,扩展性得到了很大的提高!

系统通过路由找到了真正的业务逻辑,比如首页或文章的展示,feed输出等具体业务,我们下回抓几个典型讲讲。

2015年01月17日更新:

增加action的小节。

下一节

码字不易,转载请注明出处。

上一节

上次我们讲到了系统完成了初始化,这一篇我们会更加深入typecho,同时会介绍typecho的插件机制。

插件点

热心的读者肯定会问,什么是插件点?当然你会不知道,因为这个名词是老高想出来的ಠ౪ಠ,本次要讲的插件机制的基础知识可以参考我的另一片博文thinkphp钩子的实现钩子这个词比较粗俗,其实高大上的叫法就是插件,而老高自创的插件点就是事件的意思。

index.php中Typecho_Plugin::factory('index.php')->begin(),其实就是通知挂载到'index.php'这个事件的插件可以执行了。

具体流程如下:

Typecho_Plugin::factory('index.php')返回了Typecho_Plugin的实例,构造函数中确定了唯一的句柄,即'index.php',紧接着执行了该实例的begin()方法,由于该方法不存在,所以调用了魔术方法__call,最后由__call方法执行所有在这个插件点挂载的插件。

__call方法源代码:

    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];
    }

关键点:

  • __call是可以带参数的
  • $callback是二位数组,一个类名,一个方法名
  • 在该类init方法中,activated和handles是分开保存的,因为activated以插件名做键名,而handles以插件点为键,目的不同
  • 插件的关联信息保存在数据库中typecho_options表中的plugins
  • 需要禁用再启用才能使修改后的插件生效

当完成了第一个插件点的挂载后,程序开始路由分发。

下一节

码字不易,转载请注明出处。

首先恭喜typecho v1.0的上线!

其次,这篇文章同thinkphp的源代码解析一样都是老高谋划了很久的文章,国庆节由于单位加班没有时间写,今天终于等来了轮休,果断放开了写。希望大家多多支持!

最后老高想说的是,如果大家有兴趣研究源码,那么问题来了,如何高效的学习研究源代码?

老高的建议是:

  1. 一定要熟悉MVC模式(针对WEB开发)
  2. 先看看文档再动手
  3. 分辨什么是好的坏的代码,不要搞盲目崇拜
  4. 做笔记

以下:

文档

如果有什么不明白的,文档里也许会找到答案。

typecho开发文档

版本

再研究源代码前,如果知道自己使用的typecho的版本呢?

答案写在var/Typecho/Common.php里,Typecho_Common类中的常量VERSION

例子:

class Typecho_Common
{
    /** 程序版本 */
    const VERSION = '1.0/14.10.9';
    ...
}

开启DEBUG模式

有些同学可能已经发现,typecho默认对外隐藏了PHP的错误信息,如果我们想要看到真正的报错信息,需要开启typecho的DEBUG模式,也可以叫做开发模式。当我们开启了这个模式后,在开发插件或者了解系统原理的时候就能够得到可视化的错误信息了。

开启方法:

# 修改/config.inc.php
# 在代码的第一行加入
/**开启debug模式*/
define('__TYPECHO_DEBUG__',1);

ps.调试的时候如果你的服务器没有安装xdebug,那么你的var_dump()信息会没有格式。

在此老高推荐使用TP框架内的方法dump(),以后我们就可以使用dump()打印变量信息了。

# 修改/config.inc.php
# 在debug后加入

/**
 * 浏览器友好的变量输出
 * @param mixed $var 变量
 * @param boolean $echo 是否输出 默认为True 如果为false 则返回输出字符串
 * @param string $label 标签 默认为空
 * @param boolean $strict 是否严谨 默认为true
 * @return void|string
 */
function dump($var, $echo=true, $label=null, $strict=true) {
    $label = ($label === null) ? '' : rtrim($label) . ' ';
    if (!$strict) {
        if (ini_get('html_errors')) {
            $output = print_r($var, true);
            $output = '
' . $label . htmlspecialchars($output, ENT_QUOTES) . '
'; } else { $output = $label . print_r($var, true); } } else { ob_start(); var_dump($var); $output = ob_get_clean(); if (!extension_loaded('xdebug')) { $output = preg_replace('/\]\=\>\n(\s+)/m', '] => ', $output); $output = '
' . $label . htmlspecialchars($output, ENT_QUOTES) . '
'; } } if ($echo) { echo($output); return null; }else return $output; }

入口

入口文件为index.php,国际惯例贴代码。

if (!@include_once 'config.inc.php') {
    file_exists('./install.php') ? header('Location: install.php') : print('Missing Config File');
    exit;
}

/** 初始化组件 */
Typecho_Widget::widget('Widget_Init');

/** 注册一个初始化插件 */
Typecho_Plugin::factory('index.php')->begin();

/** 开始路由分发 */
Typecho_Router::dispatch();

/** 注册一个结束插件 */
Typecho_Plugin::factory('index.php')->end();

typecho也采用了流行的单一入口,在此会引出一个伪静态的问题,此处先留个坑,以后补上。

index.php文件流程:

config.inc.php文件如果存在,就引入;如果不存在,就跳转到install.php进行程序安装,请原谅老高在此跳过install.php流程,如果读者有问题,请留言交流。

config.inc.php在安装程序的时候会自动生成,其主要工作是:引入程序常量,设定包含路径,引入各个核心文件(注意此时没有使用__autoLoad,所以只能require),执行Typecho_Common::init()初始化工作,保存数据库连接信息并初始化数据库对象。

Typecho_Common::init()位于var/Typecho/Common.php,方法代码:

public static function init()
    {
        ini_set( 'display_errors', 'On' );
        /** 设置自动载入函数 */
        if (function_exists('spl_autoload_register')) {
            spl_autoload_register(array('Typecho_Common', '__autoLoad'));
        } else {
            function __autoLoad($className) {
                Typecho_Common::__autoLoad($className);
            }
        }

        /** 兼容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'));
    }

代码中ini_set( 'display_errors', 'On' );是老高自己加的,便于debug,请无视。此方法的主要工作是注册autoload,如果系统打开了magic_quotes_gpc,就使用stripslashesDeep处理输入数据,去除系统自动加入的/,最后接管了系统的异常截获。

config.inc.php完成使命后,控制权又回到了index.php文件。

紧接着执行了Typecho_Widget::widget('Widget_Init'),Typecho_Widget哪里来的?请研究一下init阶段的autoload方法,把类名中的_\替换为/,最后加上.php,这样类名就被映射到了一个确定的文件了。以Typecho_Widget为例,系统将得到Typecho/Widget.php,由于系统核心文件都在var下,那么最终的路径即var/Typecho/Widget.php。所以我们就得到了这样的规律,类名被_分开,类一般会以Typecho或Widget开头,分别对应着文件夹Typecho或Widget,之后就根据剩下的片段去找最终的文件名即可!

@include_once str_replace(array('\\', '_'), '/', $className) . '.php';

Widget类是整个系统的核心类(抽象类),他抽象出了几个很不错的功能,这一点Widget内部方法与接口有介绍,需要注意的是魔术方法__get会在保护属性$row里查找变量,$row也让Widget类增色不少,__callpush方法也需要留意。

下面介绍的是Typecho_Widget::widget方法,widget方法其实是一个工厂方法,他接受一个类名,初始化输入输出对象,一并传入目标类名,执行了目标类的构造方法和execute方法,并将实例化的类保存在self::$_widgetPool数组中。execute十分重要,在thinkphp中有一个异曲同工的方法,叫_initalize,位于controller父类中。

以index.php文件中Typecho_Widget::widget('Widget_Init')为例,Typecho_Widget的widget接收到'Widget_Init',初始化var/Widget/Init.php中的Widget_Init类后,执行了其execute方法。该方法又调用了$this->widget('Widget_Options'),即自己又初始化了Widget_Options类,Widget_Options类读出了user = 0的所有系统配置,并将Widget_Options保存在局部变量$options里,进行了一系列的初始化工作。值得留意的地方:

# $options包含了路由信息
# 系统在这一步才执行了解析PathInfo动作
$this->request->getPathInfo();
# 路由器在此初始化
Typecho_Router::setRoutes
# 所有插件在此初始化
Typecho_Plugin::init($options->plugins);
# 下面还有初始化回执,设定了返回的编码和类型
$this->response->setCharset($options->charset);
$this->response->setContentType($options->contentType);
# 时区设置还有session初始化
# 最后打开缓冲

至此Typecho_Widget::widget('Widget_Init')执行完毕!这时系统初始化正式完毕,开始真正的'工作'!

2015年01月12日更新:

开启开发者模式

下一节

码字不易,转载请注明出处。