资源描述
ECMall 2.X 架构分析与技术指南
ECMall 2.X 架构分析与技术指南
一、系统架构分析:
根据文件目录结构分析得到ECMall为MVC系统架构。
控制器(Controllers)分析:
在ECMall中分2种控制器,FrontendApp(前端)和BackendApp(后端),我们可以依据基础控制器创建自己的控制器。
控制器文件的命名规则:类名(首字母大写) + .app + .php;
文件位置:一般放置在相应的app目录中;
类名规则:类名+App
访问规则:/index.php?app=控制器类&act=方法¶m=…
注意:第一个控制器,必须拥有自己的语言文件,如MallBaseApp的子类语言文件位于\languages\sc-gbk\下,而模块Module类的语言文件则位于模块的文件夹中,即使语言文件中仅有return array()的空数组。若没有相应的语言文件,程序将会报错。
现在我们试着写一个自己的控制器:
<?php
/**
* 控制器演示类,此处采用单入口模式,方法与模型对应;
* 与ECMall本身的多入吕,控制器模型对应有所区别;
* @author CTO
*/
class Demo1App extends MallbaseApp{
/**
* 默认控制器方法
* @return void
*/
Function index(){
Echo __METHOD__;
}
/**
* Goods方法对应Goods模型
* @return void
*/
function goods(){
//实例化goods
$goods = m(‘goods’);
$id = empty($_GET[‘id’]) ? 0 : intval($_GET[‘id’]);
if(!$id){
echo “Warning! Hacking!”;
return;
}
//获取goods信息
$goods_info = $goods->get_info($id);
//输出goods信息
$result = print_r($goods_info);
}
//此处的test自定义模型将在下面讲述,暂时略过;
function test(){
$test = m(‘test’);
$data = array(‘name’=>’建航’,”name2”=>”科技”);//name,name2对应表字段
$test->addData($data);
}
}
测试访问:/index.php?app=demo1&goods&id=3
模型(Model)分析:
ECMall模型分为业务模型与普通模型,二者皆继承自核心类的BaseModel模型;
模型、插件的继承关系
模型:可以理解为数据实体类,对应数据库表字段,一个实体表示一张表,每个实例为一行记录。ECMall的大部分模型与表有对应关系,仅业务模型没有表联系(操作业务如CRUD)。
创建自己的模型:
模型文件的命名规则:类名(首字母大写)+.model+.php;
文件位置:一般放置在相应的/includes/models目录中;
类名规则:类名+Model
调用规则:可能选用控制器对应模型的多入口方式 ,也可以选用控制器对应模型的单入口方式。
首先建立一张表Test(id,name,name2);
编写test.model.php,代码如下:
<?php
class TestModel extends BaseModel{
/**
* $table为表映射,$prikey为映射表的主键,$alias为表查询时的别名,主要体现在SQL语句里,$_name为模型的名称,这四个值都与ECMall模型数据库操作(很独到的一种操作方式,非通用的Adodb+SQL的方式)有关,如果不想用SQL语句查询的话,而是使用模型自身提供的数据操作功能,那么至少要把$table,$prikey体现出来;至于$alias与$_name的存在与否并不重要。只有类似于我们需要得到模型名称时,才会将$_name进行实现,例如:function getName(){return $this->_name;}。
*/
/**
* 增加数据演示
* @author kichijyo;
* @return void
*/
function addData($data){
$this->add($data);//该处的add()继承自BaseModel类
}
}
测试访问:/index.php?app=demo1&act=test
下面我们试着建一对主、从表的模型关系:
首先我们建2张表,一个是ecm_test从来(id,stu_id相对于ecm_test2表的外键,name),另一个为ecm_test2主表(stu_id相对于ecm_test2的主键,score)。根据约束,先删除从表,方可继续删除主表。对照这两张表开始建立模型,分别为:TestModel,Test2Model两个类:
首先我们分析下\eccore\model\model.base.php文件中的_getJoinString()方法中的几个case条件:
switch($relation_info[‘type’]){
case HAS_ONE:
//一对一关系
break;
case BELONGS_TO:
//一对多的从属关系
break;
case HAS_AND_BELONGS_TO_MANY:
//多对多的从属关系
break;
}
我们注意到,仅有3个可用,分别为HAS_ONE,BELONGS_TO,HAS_AND_BELONGS_TO_MANY,意味着HAS_MANY不可以用来做多表查询。
下面着重讲述一下\eccore\model\model.base.php:
define(‘HAS_ONE’,1); //一对一关联
define(‘BELONGS_TO’,2); //属于关联
define(‘HAS_MANY’,3); //一对多关联
define(‘HAS_AND_BELONGS_TO_MANY’,4); //多对多关联
多表查询中存在的左右关系整理如下(√表示实体模型是否可用来操作多表查询):
包含
从属
HAS_ONE √
BELONGS_TO √
HAS_MANY
BELONGS_TO √
HAS_AND_BELONGS_TO_MANY √
HAS_AND_BELONGS_TO_MANY √
虽然从HAS_ONE与BELONGS_TO两种方式执行操作多表查询的left join 不同,但是之后一经加入conditions即where条件过滤后,两个的结果完全相同了。
例如:
1、 一个人有一个身份证号,我们称一个人HAS_ONE(既有)身份证号,反过来一个身份证号属于一个人所有,我们称一个身份证号BELONGS_TO(从属反向)一个人。(一对一关系);
2、 一个班级有多个学生,我们称一个班级HAS_MANY(既有)学生,一个学生只属于一个班级,我们称一个学生BELONGS_TO(从属反向)一个班级。(一对多关系);
3、 一个学生可以选择多门课程,反过来一个课程可以被多个学生选择,我们对这两个实体模型皆用HAS_AND_BELONGS_TO_MANY。(多对多关系,既有又从属反向);
关于这三种关系,模型中涉及几个重要的字段:
“model”=>”指明对应的模型名称”,
“type”=>”指明当前模型与对应模型的关系是三种关系中的哪一种?”,
“ext_limit”=>”又称联合限制ext(扩展的意思),本质为left join … on … and XXX”扩展的即是这个语句的XXX内容”,
“foreign_key”=>”对应模型中的外键”,
“refer_key”=>”当前模型中的主键,若与对应模型外键同名,可省略不写”,
“reverse”=>”当前模型相对于对应模型关系为BELONGS_TO的从属关系时需要填写这个反向指代关系,值为对应关系的数组key值”,
“middle_table”=>”当实体模型关系出现多对多时才会出现的中间关系表”
Okay,我们开始模型设计:
1、 主表模型:
class Test2Model extends BaseModel{
var $table = ”test2”;
var $prikey = “stu_id”;
//这里同样对应着写,Test2对应test之后写test表中相对于Test2的外键与reference
var $_relation = array(
“has_test” => array(
“model”=>”test”,
“type”=>HAS_MANY,
“foreign_key”=>”stu_id”,
“refer_key”=>”stu_id”
)
);
}
2、 从表模型:
class TestModel extends BaseModel{
var $table = “test”;
var $prikey = “id”;
var $_relation = array(
//关于关系,这里按对应来写,像test对应test2,而且test为从表,故belongs_to test2表
//reverse 反向关系到test上
“belongs_to_test2” => array(
“model”=>”test2”,
“type”=>BELONGS_TO,
“revers”=”has_test”
)
);
}
3、 应用:
class Jiang3App extends MallbaseApp{
function index(){
//因为仅有3个可用,分别为HAS_ONE,BELONGS_TO,HAS_AND_BELONGS_TO_MANY
//故关系上选取拥有type为以上三个值的模型。
$test = m(“test”);
$sql = array(
‘join’=>’belongs_to_test2’,
‘conditions’=>’test.stu_id’
);
$arr = $test->find($sql);
print_r($arr);
}
}
4、 多对多应用:
Course.model.php
class CourseModel extends BaseModel{
var $table = “course”;
var $prikey = “id”;
var $alias = “cour”;
var $_name = “course”;
var $_relation = array(
“model”=>”student”,
“type”=>HAS_AND_BELONGS_TO_MANY,
“middle_table”=>”stu_course”,
“foreign_key”=>”course_id”,
“refer_key”=>”id”,
“reverse”=>”learn_course”
);
}
Student.model.php
class StudentModel extends BaseModel{
var $table = “stu”;
var $prikey = “id”;
var $alias = “stu”;
var $_name = “student”;
var $_relation = array(
“learn_course”=> array(
“model”=>”course”,
“type”=>HAS_AND_BELONGS_TO_MANY,
“middle_table”=>”stu_course”,
“foreign_key”=>”stu_id”,
“refer_key”=>”id”,
“reverse”=>”join_student”
)
);
}
应用:
class Jiang3App extends MallbaseApp{
function index(){
$course = m(“course”);
$sql = array(
‘join’=>’join_student’,
‘conditions’=>’cour.id=2’,
‘fields’=>’stu_course.stu_id’
);
$arr = $course->find($sql);
print_r($arr);
}
}
5、 模型验证:
每个模型映射表结构,那么必然有相关的约束验证,模型中的$_autov(自动验证auto validate),此处的自动验证包括类如:required(是否必需),reg(正则验证)等内容。
下面我们来看一个典型的验证模型:
一个典型的表对应模型:
class TestModel extends BaseModel{
var $table = “test”;
var $prikey = “id”;
var $_autov = array(
“title”=>array(
“required”=>true,
“filter”=>”trim”
),
“description”=>array(
“required”=>true,
“filter”=>”trim”
)
);
var $_relation = array(
“has_test2”=>array(
“model”=>”test2”,
“type”=>HAS_MANY,
“foreign_key”=>”pid”,
“refer_key”=>”id”
)
);
}
从上面我们看到有$table,$prikey,$_autov,$_relation这4个部分,扩展可以加上$_name,$_alias等;
【延伸阅读】
刚刚在上面讲述了join语句,这里有个地方需要大家注意:
数据库在通过连接两张表或多张表来返回记录时,都会生成一张中间表,然后再将这张临时表返回给用户。
在使用left join 时,on和where条件的区别:
例如:
两条SQL语句:
1、 select * from tab1 left join tab2 on (tab1.size = tab2.size) where tab2.name=’AAA’
2、 select * from tab1 left join tab2 on (tab1.size = tab2.size and tab2.name=’AAA’)
假设有两张表:
Id
Size
1
10
2
20
3
30
表tab1
size
name
10
AAA
20
BBB
30
CCC
表tab2
第一条SQL的过程:
1、 先生成中间表 on 条件:tab1.size=tab2.size
Tab1.id
Tab1.size
Tab2.size
Tab2.name
1
10
10
AAA
2
20
20
BBB
3
30
30
CCC
4
40
40
DDD
临时中间表
2、 再对中间表过滤where条件:tab2.name=’AAA’
Tab1.id
Tab1.size
Tab2.size
Tab2.name
1
10
10
AAA
最终数据表
结论:where条件是在临时表生成好后,再对临时表进行过滤的条件,这时已经没有left join的含义(必须返回左边表的记录)了,条件不为真的全部过滤掉了。
第二条SQL的过程:
1、 中间表on条件:tab1.size = tab2.size and tab2.name=’AAA’
Tab1.id
Tab1.size
Tab2.size
Tab2.name
1
10
10
AAA
2
20
Null
Null
3
30
Null
Null
最终数据表
结论:on条件是在生成临时表时使用的条件,它不管on中的条件是否为真,都会返回左边表中的记录。
其实以上结果的关键原因就是left join,right join,full join的特殊性,不管on上的条件是否为真都会返回left或right表中的记录,full则具有left和right的特性的并集。而inner join(也就是join的全称)没这个特殊性,则条件放在on中和where中,返回的结果集都是相同的。
三表(更多表联合查询)join语句:
select * from ecm_stu_s join ecm_stu_course sc join ecm_course c on s.id=sc.stu_id and sc.course_id=c.id where s.id=3;
标注:ECMall目前暂不支持类似这样的三表链接查询语句。
视图模板(Views)分析:
首先让我们看一段商城首页的部分代码:
{include file=header.html}
<div class=”keyword”>
<div class=”keyword1”></div>
<div class=”keyword2”></div>
{$lang.hot_search}:
<!--{foreach from=$hot_keywords item=keyword}-->
<a href=”{url app=search&keyword=$keyword}”>{$keyword}</a>
<!--{/foreach}-->
</div>
<div class=”content”>
<!--{widgets page=index area=top_left}-->
</div>
整体代码类似于此,这里有2个部分需要我们分析:
1、{include}或{foreach}的Smarty语法,可以参考在、相关的Smarty资料;
2、{widgets}挂件开发;
让我们先来看下挂件的管理方法:
1、 进入2.2后台,在这里寻找挂件管理界面:
挂件管理界面
2、 将设计好的挂件放入模板中;
模板管理界面
拖“挂”动作,这就是所谓的“挂”件
在图中我们看到这块灰色的区域是我在\themes\mall\default\index.html文件中设置的,一般的我们会先设计出挂件将要拖入的区域,而后拖放挂件。
这块区域的代码如下:
<div style=”height:200px; background-color:gray;” area=”mina” widget_type=”area”>
</div>
注意两点:area与widget_type属性,其中area属性用来拖放挂件时给挂件配置文件指明从属方向,这里我们看下挂件拖放后形成的配置文件代码:
文件定位:\data\page_config里面有两个配置文件,default.gcategory.config.php(商品分类页)与default.index.config.php(商城首页),我们打开商城首页代码:
部分截取:
return array(
‘widgets’=>array(
‘_widget_235’=>array(
‘name’=>’kichijyo’,
‘options’=>NULL)
),
‘_widget_877’=>array(
‘name’=>’notice’,
‘options’=>array(
‘ad_image_url’=>’data/files/mall/template/200908070207084061.gif’,
‘ad_link_url’=>’’)
),
)
);
上面是对每个挂件定义的ID值,继续向下滚动代码近于文档底部:
‘config’=> array(
‘mina’ => array(0=>’_widget_235’),
‘top_left’ => array(0=>’_widget_877’,1=>’_widget_578’,2=>’_widget_323’),
‘cycle_image’=> array(0=>’_widget_698’)
);
在这里可以看到,刚才在模板管理后台拖放挂件的时候,div标签中的area为上面这段配置代码指明了方向,就是说:{widgets page=index area=mina}里mina区域将包括ID为_widget_235这个的挂件,展示页面位于index上。{widgets page=index area=cycle_image}里cycle_image将包括ID为_widget_368的挂件,用于展示位于index的模板中。
既然系统都已经做好了配置文件,下面我们可以完善填充代码了,加入代码后开始:
<div style=”height:200px;background-color:gray;” area=”mina” widget_type=’area’>
{widgets page=index area=mina}
</div>
3、 挂件开发:
3.1、数据调用形式概述:
3.1.1、通用调用(ADODB类)
//通用Adodb数据调用描述;包含有直接返回数组的getone,getcol,getrow,getall,除query(返回资源,非数组)
//这里我使用了通用的Query方法
$db = db(‘test’);//这里我们查询test表,注意没有表前缀
$sql = “select * from ecm_test”; //注意这里有表前缀
$quer = $db->query($sql); //这里和我们的PHP很类似,返回一个资源
$arr[] = array(); //创建一个空数组,用来存放数据
while($row=$db->fetchrow($query)){
array_push($arr,$row);
}
return $arr;//返回数组;下面准备进入widget模板做Smarty
上面的SQL可以替换为:insert,delete,update等相关常规语句;
3.1.2、内部模型调用(ECMall独有,需单独研究)
使用内部模型有两步骤:一、建实体类(或业务模型);二、数据调用,比通用麻烦点,但是后面省心哦。下面继续:
让我们来创建一个模型:
class TestBModel extends BaseModel{
var $table = ‘test’; //映射表名,必须字段,注意无前缀;
var $prikey = ‘id’;//主键定义,必须字段
function getData(){}//其它方法定义
}
实例化时注意:$test=bm(‘test’);注意用bm实例化;
这里我用的BModel(操作业务模型),我们从前面的模型继承图知道,BModel继承自BaseModel,当然这里如果不使用操作业务模型,仅仅使用与表有映射关系的普通模型也是可以的,比如:
class TestModel extends BaseModel{
var $table = ‘test’;
var $prikey = ‘id’;
function getData(){}//其它方法定义
}
实例化时使用:$test=m(‘test’); //这里使用m来实例化;
模型建立完,我们开始调用模型,可以在诸如挂件、控制器等地方使用,代码如下:
以实体类为例:
function index(){
$test = m(‘test’);//实例化test模型
//这里使用了模型中比较通用的find()方法,当然你也可以使用直接返回数组的get_info($id),get($array)等模型中的函数;下面的$query直接返回是数组,不是资源了,这点与ADODB的DB不同。
$query = $test->find($array(
‘conditions’=>’id>2’,
‘limit’=>1,
‘order’=>’id desc’
));
$this->assign(‘arr’,$query);
$this->display(‘test.html’);
}
讲述到这里,数据CRUD部分算是分析完毕了;下面我们将讲述挂件设计
3.2、挂件代码设计:
挂件的组织形式按照文件夹组织,位于\external\widgets目录下,每个文件夹对应一个挂件,每个文件夹中包含4个文件:config.html(配置界面模板)、main.widget.php(挂件主程序文件)、widget.html(挂件显示模板)、widget.info.php(挂件信息文件)。我们不必自己创建,只要直接复制一个挂件文件夹即可。然后着手修改里面的代码就Okay了。
文件分析:
3.2.1、widget.info.php(信息配置文件):
return array(
‘name’=>’kichijyo’, //给挂件起名,注意应与包含文件夹的名字相同;
‘display_name’=>’客齐集’, //模板管理界面显示的挂件名字;
‘author’=>’Kichijyo Team’,
‘website’=>’’,
‘version’=>’1.0’,
‘desc’=>’显示测试数据内容’, //挂件描述Description;
‘configurable’=>true //配置本挂件是否可以配置,false时挂件不能被傻瓜配置;
);
3.2.2 main.widget.php(挂件主程序):
a) 类的命名规则:挂件文件夹名(首字母大写)+widget;类中的$_name字段,赋值为挂件文件夹名;
b) 需要关注的几个方法:
c) _get_data():该方法用来返回挂件需要展示的数据;
d) get_config_datasrc():该方法用来获取配置界面所需的展示数据;
e) function parse_config($input):该方法用来处理配置页面传入的数据;
f) 需要关注的变量:$widget_data(相当于Smarty.assign()之后的数据),$options配置的数组数据;
g) parse_config($input):该方法是配置数据的保存处理方法,默认它将数据保存到\data\page_config\default.index.config.php或另外一个商品文件中,具体的保存位置依据具体挂件模板来差别,index对应index,商品分类的对应商品分类,也就是说,它的数据保存不面向数据库,它以文本文件的形式保存在了\data\page_config\下的php文件中。如果需要保存到数据库,请自行加入数据库操作的实现处理。
3.2.3、挂件操作流程图:
挂件处理流程图
3.2.4、主程序简易代码实现:
class KichijyoWidget extends BaseWidget{
var $_name = ‘kichijyo’; //必须与文件夹同名
function _get_data(){
return $this->options; //返回$widget_data数组给widget.html模板
}
//从\data\page_config文件夹中的相应PHP文件中获取相关的options数组,并以$options来填充配置页面模板
function get_config_datasrc(){
$this->options=stripslashes_deep($this->options);
$this->assign(‘options’,$this->options);
}
//处理配置页面传入的数据,并将该数据返回给$options数组,即保存到\data\page_config文件夹下的PHP文件中;
function parse_config($input){
return $input;
}
}
3.2.5、其它文件简述:
config.html:
请输入您的姓名:<input type=”text” name=”name” value={$options.name} /><br />
请输入您的描述:<input type=”text” name=”description” value={$options.description} />
需要注意的是,这里不需要任何method=post等,仅仅写少许必要代码即可,ECMall连提交按钮都给做好了。
widget.html:
{$widget_data.name} and {$widget_data.description}
这里直接上Smarty模板语法,一切皆Okay。
至此,挂件部分描述完毕;下一节模块开发
4、 模块开发
模块的组织形式:\external\modules目录下创建模块文件夹,模块内的组织文件包括:
a) index.module.php(前端控制器)
b) admin.module.php(后端控制器)
c) module.info.php(模块配置信息文件)
d) 模板templates文件夹
e) 语言文件夹languages
对于安装与卸载文件不是必须文件,我们略过。。。
简要浏览下模块控制器\admin\app\module.app.php
这里有Install,Uninstall,Manage,Configer等等方法。
感兴趣的朋友,可以研究一下哦。
下面开始我们的开发之旅,首先和挂件一样,我们根本不需要自己逐一创建文件,只要复制一个已经有的模块文件夹并重命名即可。这里我复制DataCall文件夹并重命名为Kichijyo。
步骤:
1. 修改模块配置文件Module.info.php;
<?php
return array(‘id’ => ‘kichijyo’,//这里应保证与模块文件夹同名。
//这里是调用语言配置文件
//external\modules\kichijoy\languages\sc-utf-8\common.php
//或\languages\sc-utf-8\admin\module.lang.php
‘name’ => Lang::get(‘data_call’),
‘desc’ =>Lang::gete(‘datacall_desc’),
‘version’=>’2.0’,
‘author’ =>’Kichijyo Team’,
‘Website’ =>’’,
‘menu’=>array(
array( ‘text’ => Lang::get(‘datacall_manage’),
‘act’=>’index’,
),
),
);
?>
通过上面的设置,我们在模块管理界的安装连接上已经改变了:
/admin/index.php?app=module&act=install&id=kichijyo
通过id=kichijyo使得安装模块指向了我们新创建的Kichijyo。
下面是部分的语言文件:
\external\modules\kichijyo\languages\sc-utf-8\common.lang.php
<?php
return array(
‘detacall_desc’=>”这是客齐集演示用模块”,
‘data_call’=>’客齐集模块’,
‘manage_data’=>’管理’,
‘add_goods’=>’新增商品调用’,
‘belong_type’=>’所属类型’,
‘handler’=>’操作’,
‘data_goods’=>’商品数据’);
?>
2. 修改前端控制器Index.module.php
所有的模块Module都是ECBaseApp的子类,即模块也是控制器;
类的命名规则:模块名(首字母)+Module 扩展自 IndexBaseModule。
<?php
//前端访问形式往往是体现为JSON的数据调用形式,如:
//<script type=”text/javascript” src=”/index.php?module=kichijyo”></script>
//可以用来给外部系统提供js的数据调出。
class KichijyoModule extends IndexbaseModule
{
//index()为默认方法,延续了所有的控制器的特征;
function index()
{
$db=db(“test”);
$sql=”select * from cem_test”;
$query=$db->query($sql);
$result=array();
while($row=$db->fetchrow($query)){
$result[]=$row;
}
$this->assign(“result”,ecm_json_encode($result));//注意这里的ECM_JSON
$this->display(“admin/datacall.index.html”);
}
}
?>
与上部匹配的前端调用模板,我们可以这样写:
\external\modules\kichijyo\templates\admin\datacall.index.html
var result={$result};
for(var i=0;i<result.length;i++){
document.write(result[i][“description”]+”<br/>”);
}
此种方式为JS嵌入式的写法,上面皆是JS语法规则;当我们把<script type=”text/javascript” src=”/index.php?module=kichijyo”></script>代码加入到需要跨系统调用数据的页面时,系统将执行上面的这段JS代码,将数据调出给外部系统。
3. 修改后端的控制器的Admin.module.php类的命名规则:模块名(首字母)+Module 扩展自AdminBaseModule。
<?php
//后端访问的URL形式,具体体现在模块管理的链接地址:
展开阅读全文