lwbaptx

opencart整体架构分析

opencart是一个完全免费的开源系统,采用MVCL的框架

整体概述

1:opencart是单一入口系统,所有的请求都要经过index.php文件来处理

2:系统框架对当前的url进行分析,找到对应的controller(控制器)和action(动作),去调用控制器文件里面的动作方法,url路径是调用各个文件的关键

3:在action动作中可以读取数据,处理业务逻辑,然后把完成的数据渲染到视图,给终端用户呈现出来

目录结构

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
29
30
31
32
33
34
35
36
37
38
39
40
41
├── admin                   管理后台目录
│ ├── config.php 配置文件
│ ├── controller 控制器目录
│ ├── index.php 入口文件
│ ├── language 语言目录
│ ├── model model目录
│ ├── php.ini php配置文件
│ └── view 模板目录
├── catalog 商店前台目录
│ ├── controller 控制器目录
│ ├── language 语言目录
│ ├── model model目录
│ └── view 模板目录
├── config.php 配置文件
├── image 图片目录
│ ├── cache 图片缓存目录
│ ├── catalog 图片上传目录
│ ├── no_image.png
│ ├── payment
│ └── placeholder.png
├── index.php 商店前台入口文件
├── install 安装目录
│ ├── cli_install.php
│ ├── controller
│ ├── index.php
│ ├── language
│ ├── model
│ ├── opencart.sql
│ ├── php.ini
│ └── view
├── maintenance.html
├── php.ini
└── system 系统框架目录
├── config 配置文件目录
├── engine 引擎目录
├── framework.php 框架文件
├── helper 一些帮助文件
├── library 一些函数库
├── modification.xml
├── startup.php 启动文件
└── storage 日志和缓存数据目录

流程分析

我们来从源码的角度看一下,(注:源码只截取关键部分)

在浏览器端键入如下的opencart官网的url地址,到底会发生什么事情

1
https://demo.opencart.com/index.php?route=common/home

用户的http请求首先会被转发到这个index.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
// Version
define('VERSION', '2.3.0.3_rc'); //版本号

// Configuration //包含config.php文件进来
if (is_file('config.php')) {
require_once('config.php');
}

// Install //没有安装完成就会走安装的步骤
if (!defined('DIR_APPLICATION')) {
header('Location: install/index.php');
exit;
}

// Startup
require_once(DIR_SYSTEM . 'startup.php'); //包含startup.php文件进来

start('catalog'); //调用startup.php中的start函数

再看一下startup.php文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Engine
//把engine目录下面的文件包含进来
require_once(modification(DIR_SYSTEM . 'engine/action.php'));
require_once(modification(DIR_SYSTEM . 'engine/controller.php'));
require_once(modification(DIR_SYSTEM . 'engine/event.php'));
require_once(modification(DIR_SYSTEM . 'engine/front.php'));
require_once(modification(DIR_SYSTEM . 'engine/loader.php'));
require_once(modification(DIR_SYSTEM . 'engine/model.php'));
require_once(modification(DIR_SYSTEM . 'engine/registry.php'));
require_once(modification(DIR_SYSTEM . 'engine/proxy.php'));

// Helper
// 把helper目录下面的文件包含进来
require_once(DIR_SYSTEM . 'helper/general.php');
require_once(DIR_SYSTEM . 'helper/utf8.php');
require_once(DIR_SYSTEM . 'helper/json.php');

function start($application_config) {
// 这是上面index.php调用的函数
// 这里又调用了framework.php
require_once(DIR_SYSTEM . 'framework.php');
}

再看一下framework.php文件,这里会构造各种核心的类

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<?php
// Registry
// 单例模式,保存一些全局变量
$registry = new Registry();

// Config
$config = new Config();
// 加载system/config/default.php
$config->load('default');
// 加载system/config/catalog.php
$config->load($application_config);
$registry->set('config', $config);

// Event
$event = new Event($registry);
$registry->set('event', $event);

// Event Register
// 一些预定义的拦截事件
if ($config->has('action_event')) {
foreach ($config->get('action_event') as $key => $value) {
$event->register($key, new Action($value));
}
}

// Loader
$loader = new Loader($registry);
$registry->set('load', $loader);

// Request
$registry->set('request', new Request());

// Response
$response = new Response();
$response->addHeader('Content-Type: text/html; charset=utf-8');
$registry->set('response', $response);

// Database
...

// Front Controller
// 构造一个系统控制器Front
$controller = new Front($registry);

// Dispatch
// 控制器分发action
// default.php配置文件里面
// 'action_router' = 'startup/router'
// 'action_error' = 'error/not_found'
$controller->dispatch(new Action($config->get('action_router')), new Action($config->get('action_error')));

// Output
// 返回数据给用户
$response->setCompression($config->get('config_compression'));
$response->output();

看一下Front这个系统controller里面是怎么查找具体的action的

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
29
30
31
32
33
34
35
36
<?php
final class Front {
private $registry;
private $pre_action = array();
private $error;

public function __construct($registry) {
$this->registry = $registry;
}

public function dispatch(Action $action, Action $error) {
$this->error = $error;

...

while ($action instanceof Action) {
$action = $this->execute($action); //转调execute
}
}

private function execute(Action $action) {
$result = $action->execute($this->registry); //调用Action的execute方法

if ($result instanceof Action) {
return $result;
}

if ($result instanceof Exception) {
$action = $this->error;

$this->error = null;

return $action;
}
}
}

看一下Action是的execute方法

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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
<?php
class Action {
private $id;
private $route;
private $method = 'index'; //默认的函数是index

public function __construct($route) {
$this->id = $route;

$parts = explode('/', preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route));

// Break apart the route
while ($parts) {
$file = DIR_APPLICATION . 'controller/' . implode('/', $parts) . '.php';

if (is_file($file)) {
$this->route = implode('/', $parts);

break;
} else {
$this->method = array_pop($parts); //找到controller后面的第一个part就是函数
}
}
}
public function execute($registry, array $args = array()) {
// 这里的$this->route就是上面传入的'startup/router'
// DIR_APPLICATION 是catalog的目录
// 这个$file最终就是找到catalog/controller/startup/router.php文件
$file = DIR_APPLICATION . 'controller/' . $this->route . '.php';
//$class构造出ControllerStartupRouter这个类
$class = 'Controller' . preg_replace('/[^a-zA-Z0-9]/', '', $this->route);

// Initialize the class
if (is_file($file)) {
include_once($file);

$controller = new $class($registry); //构造类
} else {
return new \Exception('Error: Could not call ' . $this->route . '/' . $this->method . '!');
}

$reflection = new ReflectionClass($class);

if ($reflection->hasMethod($this->method) && $reflection->getMethod($this->method)->getNumberOfRequiredParameters() <= count($args)) {
//调controller相应的函数方法
return call_user_func_array(array($controller, $this->method), $args);
} else {
return new \Exception('Error: Could not call ' . $this->route . '/' . $this->method . '!');
}
}

下面看一下startup/router这个控制器的实现

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
29
30
31
32
33
34
35
36
37
38
39
<?php
class ControllerStartupRouter extends Controller {
public function index() {
// Route
// https://demo.opencart.com/index.php?route=common/home
// 获取url地址中的route值route=common/home
if (isset($this->request->get['route']) && $this->request->get['route'] != 'startup/router') {
$route = $this->request->get['route'];
} else {
$route = $this->config->get('action_default');
}

// Sanitize the call
$route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);

// Trigger the pre events
$result = $this->event->trigger('controller/' . $route . '/before', array(&$route, &$data));

if (!is_null($result)) {
return $result;
}

// We dont want to use the loader class as it would make an controller callable.
//构造一个'common/home'的action,参考上面action的分析
$action = new Action($route);

// Any output needs to be another Action object.
$output = $action->execute($this->registry);

// Trigger the post events
$result = $this->event->trigger('controller/' . $route . '/after', array(&$route, &$data, &$output));

if (!is_null($result)) {
return $result;
}

return $output;
}
}

下面看一下common/home的controller代码具体做的事情

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
class ControllerCommonHome extends Controller {
public function index() {
$this->document->setTitle($this->config->get('config_meta_title'));
$this->document->setDescription($this->config->get('config_meta_description'));
$this->document->setKeywords($this->config->get('config_meta_keyword'));

if (isset($this->request->get['route'])) {
$this->document->addLink($this->config->get('config_url'), 'canonical');
}

// 取数据,把数据保存到data变量中,在view模板中使用
$data['column_left'] = $this->load->controller('common/column_left');
$data['column_right'] = $this->load->controller('common/column_right');
$data['content_top'] = $this->load->controller('common/content_top');
$data['content_bottom'] = $this->load->controller('common/content_bottom');
$data['footer'] = $this->load->controller('common/footer');
$data['header'] = $this->load->controller('common/header');

// 用数据渲染模板
// 把模板输出到response对象中
$this->response->setOutput($this->load->view('common/home', $data));
}
}

再来看一下在loader.php里面,view是怎么加载的

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
29
30
31
32
33
34
35
36
<?php
final class Loader {
public function view($route, $data = array()) {
$output = null;

// Sanitize the call
$route = preg_replace('/[^a-zA-Z0-9_\/]/', '', (string)$route);

// Trigger the pre events
// 加载预定义的模板,默认模板也是在这里加载的
$result = $this->registry->get('event')->trigger('view/' . $route . '/before', array(&$route, &$data, &$output));

if ($result) {
return $result;
}

if (!$output) {
//构造Template对象
$template = new Template($this->registry->get('config')->get('template_type'));

foreach ($data as $key => $value) {
$template->set($key, $value);
}
//调用template对象的render函数
$output = $template->render($route . '.tpl');
}

// Trigger the post events
$result = $this->registry->get('event')->trigger('view/' . $route . '/after', array(&$route, &$data, &$output));

if ($result) {
return $result;
}

return $output;
}

再看下Template是怎么渲染模板的,这里是用php原生方式来渲染

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
<?php
namespace Template;
final class PHP {
private $data = array();

public function set($key, $value) {
$this->data[$key] = $value;
}

public function render($template) {
$file = DIR_TEMPLATE . $template; //找到对应的模板文件

if (is_file($file)) {
extract($this->data);

ob_start();

require($file); //把文件包含进来

return ob_get_clean(); //返回文件内容
}

trigger_error('Error: Could not load template ' . $file . '!');
exit();
}
}

最后response对象把数据echo给用户

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
class Response {
//重定向
public function redirect($url, $status = 302) {
header('Location: ' . str_replace(array('&amp;', "\n", "\r"), array('&', '', ''), $url), true, $status);
exit();
}
//把数据echo给用户
public function output() {
if ($this->output) {
$output = $this->level ? $this->compress($this->output, $this->level) : $this->output;
if (!headers_sent()) {
foreach ($this->headers as $header) {
header($header, true);
}
}
echo $output;
}
}