【技术分享】Joomla 框架的程序执行流程及目录结构分析

http://p0.qhimg.com/t01da8e443514b40d64.png

作者:Lucifaer

预估稿费:400RMB

投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿


0x00 文件目录介绍

目录

administrator/   # 管理后台目录
bin/             # 该文件夹存放一些基于Joomla框架开发的一些实用的脚本
cache/           # 文件缓存目录
cli/             # 该文件夹存放一些终端使用的命令,用于操作当前的站点
components/      # Joomla组件目录
images/          # 网站内容使用的媒体文件目录,后台有对此文件夹进行管理的功能
includes/        # 运行Joomla需要包含的基础文件
language/        # 语言目录,多语言的翻译都存放在这里
layouts/         # 应该是控制布局的,没有注意过是哪个版本加上的,也没研究过,等有时间了研究一下再写
libraries/       # Joomla使用的库文件
logs/            # 日志目录,一些异常处理都会存放在这个文件夹里,例如后台登录时输入错误的用户名和密码
media/           # Joomla使用到的媒体文件,主要是页面渲染会用到的,存放的内容跟images目录有区别,而且后台是没有对其进行管理的功能的
modules/         # Joomla模块目录
plugins/         # Joomla插件目录
templates/       # Joomla站点模板目录
tmp/             # 临时目录,如安装组件或模块时残留的解压文件等

文件

configuration.php   # Joomla配置文件
htaccess.txt        # 帮助我们生成.htaccess
index.php           # Joomla单入口文件
LICENSE.txt         # 不多叙述
README.txt          # 不多叙述
robots.txt          # 搜索引擎爬行使用的文件
web.config.txt      # 据说是IIS使用的文件

0x01 Joomla的MVC

在Joomla中并不像国内的一些cms一样,主要功能的实现放在组件中,下面就说一说Joomla中的四个非常重要的东西:组件、模块、控制器、视图。

1. 组件

在Joomla中,组件可以说是最大的功能模块。一个组件分为两部分:前台和后台。后台主要用于对对应内容的管理,前台主要用于前台页面的呈现和响应各种操作。其文件目录分别对应于joomla/administrator/components和joomla/components。组件有自己的命名规则,文件夹名须命名为com_组件名,组件的访问也是单文件入口,入口文件为com_组件名/组件名.php。如components/com_content/content.php。

其中option=com_content&view=article&id=7,它会先调用content.php,再由router.php路由到article视图,再调用相应的Model层取出ID=7的分类信息,渲染之后呈现在模板中的jdoc:include type=”component位置上。

2. 模块

与组件(Component)不同的是,模块(Module)是不能通过URL直接访问的,而是通过后台对模块的设置,根据菜单ID(URL中的Itemid)来判断当前页面应该加载哪些模块。所以它主要用于显示内容,而一些表单提交后的处理动作一般是放在组件中去处理的。因此,模块通常都是比较简单的程序,文件结构也很清晰易懂,如modules/mod_login模块中的文件结构如下:

modlogin.xml # 模块配置及安装使用的文件
mod_login.php # 模块入口文件,以mod模块名.php命名,可以看作Controller层
helper.php # 辅助文件,通常数据操作会放在这里,可以看作Model层
tmpl/ # 模板文件夹,View层
| default.php # 默认模板
| default_logout.php # 退出登录模板

2.1 模块调用的另外一个参数

在模板的首页文件中,我们会看到调用模块时有如下代码

jdoc:include type="modules" name="position-7" style="well"

这里多了一个style参数,这个其实是一个显示前的预处理动作,在当前模板文件夹中的html/modules.php中定义,打开这个文件我们就能看到有一个modChrome_well的函数,程序不是很复杂,只是在显示前对html做了下预处理。

2.2 模块的另外一种调用方法

有时候会需要在程序里调用一个模块来显示,可以用以下程序来调用

该程序会显示所有设置在position位置上的模块,当然也会根据菜单ID来判断是否加载

$modules = & JModuleHelper::getModules('position');
foreach($modules as $module){
    echo JModuleHelper::renderModule($module, array('style' = 'well'))
}

3.模板

个人理解,模板就相当于输出的一种格式。也就是在后端已经调用了相关的数据,准备在前端以什么样的格式输出。

在Joomla中,一个页面只能有一个主要内容(组件:component),其他均属于模块。如图:

http://p3.qhimg.com/t0152677c5c633da6ee.png

如果从代码来分析的话,打开index.php(组件下的index.php),除了简单的HTML和php外,还可以看到以下几类语句:

jdoc:include type="head"
jdoc:include type="modules" name="position-1" style="none"
jdoc:include type="message"
jdoc:include type="component"

这些是Joomla引入内容的方式,Joomla模板引擎会解析这些语句,抓取对应的内容渲染到模板中,组成一个页面。type指明要包含的内容的类型:

head        # 页面头文件(包括css/javascript/meta标签),注意这里不是指网站内容的头部
modules     # 模块
message     # 提示消息
component   # 组件

从代码中也可以看出,页面里只有一个component,同时有许多个modules。事实上message也是一个module,只是是一个比较特殊的module。

以http://127.0.0.1:9999/index.php?option=com_content&view=article&id=7:article-en-gb&catid=10&lang=en&Itemid=116为例从URL来分析模板内容的话,可以清晰的看出:在Joomla的URL中,重要的信息通常包含两部分:组件信息、菜单ID:

option=com_content  # 该页面内要使用的组件,后台对应到Components中,文件使用JOOMLAROOT components中的文件
view=article       # 组件内要使用的view
id=7               # view对应的ID
Itemid=116          # 该页面对应的菜单ID

所以上面URL的意思就是告诉Joomla:当前页面是要显示一个文章分类页面,分类ID是7,对应的菜单ID是116。

最后附一张图,帮助理解:

http://p0.qhimg.com/t017731f50da99ffceb.png


0x02 整体大致运行流程

1. 框架核心代码的初始化

/includes/defines.php定义各个功能模块的目录

/includes/framework.php整个框架调度的核心代码与cms运行的核心代码,框架初始化的入口。

/libraries/import.legacy.php开启自动加载类,并向注册队列注册cms核心类。

调用了JLoader中的setup方法;spl_autoload_register使其进行类的初始定义。

spl_autoload_register()是PHP自带的系统函数,其主要完成的功能就是注册给定的函数作为__autoload的实现。即将函数注册到SPL__autoload函数队列中。如果该队列尚未激活,则激活它们。

/libraries/loader.php定义了JLoader实现类的注册,加载,相关文件的包含等操作。

其中load方法从注册队列中寻找需要被自动加载的类,并包含该注册队列的值。

_autoload方法从注册队列中的prefixes的J中选取需要加载的类目录的前缀。[0]=>/joomla/libraries/joomla,[1]=>/joomla/libraries/legacy

_load方法完成了绝对路径的拼接,及相关文件的包含

/cms.php将PHP Composer生成的加载器autoload_static.php、/autoload_namespaces.php、/autoload_psr4.php、/autoload_classmap.php中的内容全部导入一个$loader的数组,之后将该数组中的前缀及所有类,注册到注册队列中,以方便使用。而这些类,都是针对于cms本身的操作的。接着开始设置异常处理以及一个消息处理器(日志)。最后,将一些注册类的名字规范为autoloader的规则。

configuration.php配置项

之后设置报错的格式

最终的注册队列:

http://p3.qhimg.com/t017eb42e336d78a4ee.png

2. 设置分析器,记下使用方法并在分析器后加标记对应代码

对应代码:

JDEBUG ? JProfiler::getInstance('Application')->setStart($startTime, $startMem)->mark('afterLoad') : null;

3. 实例化应用程序

对应代码:

$app = JFactory::getApplication('site');

在这边可能会有疑问,为什么会直接实例化一个之前没有引入的类(同样也没有包含相应的文件)。

还记得我们之前看到过的自动加载类么,在这里,我们首先发现没有在classmap中寻找到,之后在/libraries目录,以/libraries/cms/目录为查找目录,在该目录查找是否存在factory.php文件,若找到,则将该文件包含进来。

在factory.php中,会首先检查我们是否已经创建了一个JApplicationCms对象,如果未创建该对象,则创建该对象。最后创建为JApplicationSite,并将这个对象实例化(对象位于/libraries/cms/application/site.php)。

在该文件中,首先注册了application(这边是site)的名称与ID,之后执行父构造函数和“祖父“构造函数。

为了清晰的说明Joomla web应用的实例化过程,我们列一个树状图来看

|-web.php “祖父”
|--cms.php 父
|---site.php 子
web.php

完成了应用的最基础功能,包括:

返回对全局JApplicationWeb对象的引用,仅在不存在的情况下创建它

初始化应用程序

运行应用程序

对模板的渲染(文档缓冲区推入模板的过程占位符,从文档中检索数据并将其推入应用程序响应缓冲区。)

检查浏览器的接受编码,并尽可能的将发送给客户端的数据进行压缩。

将应用程序响应发送给客户端

URL的重定向

应用程序配置对象的加载

设置/获取响应的可缓存状态

设置响应头的获取、发送与设置等基本功能

首先在web.php中实例化了JInput对象。并将config指向JoomlaRegistryRegistry。接着,创建了一个应用程序程序的网络客户端,用于进行网络请求的操作。同时将已经指向的config导入,设置执行时间,初始化请求对象,并配置系统的URIs。

在cms.php中实例化了调度器,主要完成对于组件及模块的调度。并对session进行设置和初始化。

完成了以上所有的配置后,将已经配置完毕的应用对象返回到/joomla/libraries/joomla/factory.php中。完成应用对象的初始化。

4. 执行应用

调用web.php中的execute()方法完成应用的执行。


0x03 说一下我们的关心的路由问题

那么,我们的路由在框架中到底是怎样解析的呢?

其实在跟实例化应用的时候,当执行/joomla/libraries/joomla/application/web.php构造函数时,我们就可以看到Joomla对于URI的处理了:

$this->loadSystemUris();

跟进看一下loadSystemUris方法,不难看到这一句:

http://p4.qhimg.com/t0112da3727573c5a40.png

跟进detectRequestUri,发现首先判断了URI是否是http还是https,之后看到这句:

if (!empty($_SERVER['PHP_SELF']) && !empty($_SERVER['REQUEST_URI']))
        {
            // The URI is built from the HTTP_HOST and REQUEST_URI environment variables in an Apache environment.
            $uri = $scheme . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
        }

就是在这里将$_SERVER['REQUEST_URI']中的相对路径与$scheme . $_SERVER['HTTP_HOST']拼接成了完整的URI:

http://p8.qhimg.com/t012c04d2fe81e03c1e.png

完成了完整路径获取后,开始修改对象的属性,将新获得的request.uri添加进入配置列表中:

http://p5.qhimg.com/t016eed68516fac2829.png

下一步,就是遍历配置列表,查看是否已经设置了显示URI,在配置列表中键值为site_uri。显然我们现在并没有设置该选项:

http://p2.qhimg.com/t015e7327498d3d28ef.png

之后完成的操作就是要设置该显示URI。我们继续跟进一下:

http://p7.qhimg.com/t011ee981071767250c.png

跟进到joomla/libraries/vendor/joomla/uri/src/UriHelper.php的时候,我们稍停一下,看到进入了parse_url方法中。在这个方法中,首先对传入的URL进行了双重过滤,之后利用PHP自带方法parse_url,对URL进行了分割处理并保存到一个数组中,接着返回该数组:

http://p4.qhimg.com/t01c5d9ce6c4fbc7bdc.png

http://p1.qhimg.com/t01fc208f14ca62bf9e.png

最后的处理结果为:

option=com_content&view=article&id=7:article-en-gb&catid=10&lang=en&Itemid=116

处理完我们的显示URL后,在调用joomla/libraries/cms/application/cms.php中的execute方法时,在调用doExecute方法的时候,会使用joomla/libraries/cms/application/site.php文件中的route方法,这个方法将路由到我们application中。

在joomla/libraries/cms/application/cms.php中的route方法中,我们首先获取了全部的request URI,之后在getRouter方法中初始化并实例化了joomla/libraries/cms/router/router.php中的JRouter类,该类完成了对我们路由参数的识别与划分:

http://p5.qhimg.com/t0124164831229a08de.png

最后在joomla/libraries/cms/router/site.php中的parse方法中完成了相关组件的路由:

http://p7.qhimg.com/t01ba85d40ba6a2f459.png

可以明显的看到,在

$component = $this->JComponentHelper::getComponents()

后,$component的值:

http://p2.qhimg.com/t0139ba3e13afa5b683.png

对比components/目录下的组件,发现已经将所有的组件遍历,并保存在数组中。

接着遍历该数组,对每个组件设置本地路由,并包含响应的文件,从而完成路由控制。

http://p0.qhimg.com/t013ed2a57dca9456c8.png


0x04 总结一下

Joomla整体的运行思路可以简单的归结为一下几点:

框架核心代码的初始化:

关键是初始化了类自动加载器与消息处理器,并完成了配置文件的配置与导入。

完成了这一步,就可以通过类的自动加载器来实现核心类的查找与调用。自动加载器成为了cms的一个工具。

实例化应用程序:

这一步可以简单的理解为对Joomla接下来要提供的web服务的预加,与定义。

应用的执行:

这一步基于上面两步的准备,将执行应用。从代码上来看可以容易的总结出来一个规律:

预加载“执行之前需要做的事件”

执行应用

执行“执行之后要做的事件”

基本上都是以这样的形式来完成调用以及运行的。

以上都是小菜个人看法,可能有不准确或者非常模糊的地方,希望大牛们多给建议…

(完)