源码位于github:https://github.com/lvyahui8/dbuilder.git 。文中图片如果太小看不清楚,请右键点击“在新标签页中打开”即可看到原图

有兴趣还可以加QQ群交流:146103720 DBuilder交流群

第一章           引言

1.1 研究背景及意义

计算机软件技术发展至今,数据库已成为最广泛使用的存储格式化数据的媒介,数据库程序开发技术也日趋完善,大型的ORM框架使得数据库程序开发变得简单,并已成为操作关系型数据库的主流方式。数据库程序主要代码为CRUD(create, retrieve, update, delete)代码,随着ORM框架功能的完善,编写CRUD代码也衍生其固定的流程,对不同数据库表进行操作的CRUD代码也存在高度可重用性。当前编写重复性的CRUD代码成为开发人员的常态,不仅严重降低其积极性,而且损失其开发效率,所以迫切需要一种能够快速生成CRUD代码的产品,以期减少这方面的工作,提高开发效率。

1.2 研究现状

目前国外已经诞生一些解决上述需求的、具有很高可用性的CRUD生成器产品:CrudKit,CRUD-Admin-Generator,Dadabik,GroceryCrud,SximoBuilder。这些产品各有其特点,但也有一共同点:都是基于PHP进行开发(这在一定程度上决定于PHP语法的灵活性及其解析性)。SximoBuilder是其中的典型代表,尽管SximoBuilder设计独特、可用性高、流行度高,但也存在如下不足之处:

  • 不支持自定义表单控件;
  • 不支持多数据库;
  • 验证规则不完善,不支持异步验证;
  • 代码冗余度极大。

然而对于当今日益复杂的web程序,上述几点是开发过程必须考虑的问题,因此,开发一款既具有SximoBuilder现有功能、又完善其不足之处的CRUD生成器产品,势在必行。

1.3 研究内容

基于国内外CRUD生成器研究现状,笔者开发一款名为DBuilder(D为DataAdministrator的简写)的CRUD 生成器。

DBuilder借鉴SximoBuilder的模块为代码单元、由模板生成代码的思想,但拥有与SximoBuilder完全不同的代码生成器逻辑。它在实现SximoBuilder核心的代码生成、通用CRUD两种功能的基础上,通过重写代码逻辑完善其不足之处:代码冗余度大、缺少前端验证。

第二章           DBuilder系统分析

DBuilder面向的主要用户人群为web后台管理员以及开发人员,因此其系统分析过程,将更多的站在web后台管理员及开发人员的角度考虑问题。

2.1 需求分析

项目需要实现如下几点核心功能。

1)        数据源管理

用户可以在界面为项目配置多个数据源。配置的数据源信息包括数据库类型、地址、数据库名、端口、用户名、密码等信息。支持对数据源的增删改查,保证对数据源的增删改查不轻易造成系统问题。

2)        代码生成

此功能是DBuilder的核心要实现的功能,用户在选择数据源和数据表之后,能够对数据库表的字段做简单配置,配置包括Form表单配置、List(Table)列表配置、关系配置、全局属性配置。配置完成后DBuilder要能生成对数据库表的CRUD的MVC代码,即需要实现CRUD可用功能,但不用编写代码。

3)        数据库表CRUD

生成的代码必须支持数据表记录的新建、删除、更新、查询操作。

4)        菜单管理

DBuilder要能将生成的代码跟一个菜单项绑定,让用户点击菜单项之后,就可以使用DBuilder生成的CRUD功能。此菜单必须包括后台菜单,前台菜单不是必须的。

5)        用户管理

用户要实现多种角色。必须能够以邮箱为用户唯一标识,并作为登录参数。未来还要实现支持QQ、微信、新浪微博基于OAuth2.0的互联登录。

6)        权限管理

DBuilder要能实现不同用户角色对不同CRUD代码的执行、访问权限做到三维的可配置。譬如,现有一个文章管理的CRUD功能模块,用户角色分为系统管理员(SuperAdmin),管理员(Admin),访客(Guest),那么DBuilder要能实现如下的三维权限配置,且将之作为所有Module的默认权限。

表2-1 Module权限配置表

用户组与权限

查看

编辑

删除

导出

SuperAdmin

Admin

Guest

7)        站点参数配置

DBuilder作为一个网站的web后台程序,对站点的全局参数配置也是必须的,这些参数包括网站名字、关键词、联系地址、友情链接等等。

8)        操作日志

DBuilder要记录用户的操作信息,包括访问的页面、执行的CRUD类型、时间等等信息。日志的记录形式支持数据库和文件两种方式。

9)        多语言支持

DBuilder要支持多国语言的切换。至少应该支持中文和英语两种语言,且以中文为默认。

10)    多数据库类型支持

DBuilder要支持多种类型数据库,暂时主要支持关系型数据库,包括mysql,MS SqlServer,oracle,PostGreSQL等等。

2.2 数据原型分析

按照需求提取可得实体有:用户、用户组、数据源、代码模块、菜单,关系有:权限、日志。实体与关系的含义如下:

  • 用户:表示使用DBuilder的用户;
  • 用户组:表示用户的类型分组,用户类型应该至少包括访客、管理员、超级管理员三种;
  • 数据源:表示DBuilder包含的数据库配置,一个数据源的配置包含连接一个数据库所需的基本参数;
  • 代码模块:表示DBuilder生成的代码模块,描述了代码文件和配置;
  • 菜单:表示DBuilder的左侧菜单项;
  • 权限:表示用户组对每个代码模块的各种操作权限;
  • 日志:表示用户对每个代码模块的CRUD访问日志。

实体与关系的ER图如下:

图2-1 ER图

2.3 原则性要求

DBuilder应该要做成一套高性能、高可用的CRUD生成器,为此DBuilder设计中应该符合下面几项原则:

  • DBuilder要精确到每个数据库字段可配置;
  • 应具备一个WEB后台应用的雏形,使用户可在此基础上快速建立完整的WEB后台应用;
  • DBuilder要尽可能减少SQL操作,必要时可借助缓存、异步等技术,减少请求的处理逻辑,提高页面效率,减少用户等待时间;
  • DBuilder要有美观、简洁、直观的用户界面;
  • DBuilder要留有大量的扩展接口,能够让用户通过二次开发快速实现较为复杂的功能。

第三章           DBuilder系统设计

3.1 系统架构

DBuilder有下面2个核心的构件Core CRUD 模块和GModule,GModule对Core CRUD 模块有继承依赖的关系,GModule由MVC Code和CRUD Config组成;Core CRUD模块是手工编写的代码,而GModule是DBuilder生成的代码;Core CRUD 模块实现CRUD操作,GModule实现扩展功能。下图表示了这两个构件的组成和关系

图3-1概念与构件

下面对图中设计的概念、构件、模块关系以及Build与CRUD流程做详细阐述。

3.1.1 Core CRUD 模块

Core CRUD 模块实现核心CRUD操作,一切对GModule MVC中Controller的CRUD请求,最终转交至Core CRUD 模块进行处理。Core CRUD 模块会开放一些预处理和后处理接口交由GModule实现,这些接口会在Model,Controller,View上都有体现。

Core CRUD 模块主要包括如下文件

  • app/controllers/admin/AdminController.php
  • app/models/BaseModel.php
  • app/config/crud/admin.php
  • app/views/admin/core/list.blade.php
  • app/views/admin/core/form.blade.php

Core CRUD 模块读取GModule Configuration实现真正的CRUD操作。

3.1.2 GModule

GModule(Generated Module)不但实现了Core CRUD Module接口(MVC代码),而且具有自己配置文件(CRUD Configuration)。每一GModule表示以一张数据库表为主表,具备CRUD功能的代码文件合集(包括对应的MVC + Configuration代码)。譬如,DBuilder生成的一个GModule, 主表为core数据源user表,名字为User,那么User GModule应包含下面代码文件:

  • controllers/UserController.php
  • models/User.php
  • views/user/_list.blade.php
  • views/user/_form.blade.php
  • views/user/view.blade.php
  • config/crud/user.php

代码文件命名取决于GModule的名字,故为保证生成的代码文件不冲突,取GModule的名字(GModule Key,GModule Name)作为GModule的唯一标识。每一个GModule的信息都被保存在数据库中。一次新建 GModule操作将会新建上述所有代码文件,更新相关文件,并插入一条GModule记录到数据库。一次更新 GModule操作将只会更新Configuration文件。

GModule 由MVC代码和CRUD Configuration代码组成,下面分别进行阐述:

  • MVC代码:用来实现扩展接口。CRUD请求应最先路由到GModule MVC的中的Controller(控制器)。并且GModule MVC 应与Core CRUD Module的MVC代码有继承关系。
  • CRUD Configuration代码:实现对GModule主表增删改查参数的配置。该文件放置在app/config/crud/目录下,以php array的格式定义。它包含对所有字段的表单,列表,视图,关系等参数的配置,以及全局的参数配置。

GModule并不表示具体某一个模块,而是代指一类模块,这种模块可以由DBuilder生成,或者由开发人员手工建立。它主要用来实现Core CRUD Module的接口,主要包括下述几部分

1)        Controller接口

假设GModule模块的 Controller为A,Core CRUD Module 的Controller为B,则A应继承自B。CRUD请求会先路由到A,而实际的处理者是B。A会实现B开放的下列接口。

  • beforeListExcuteQuery(&querier):该接口在List查询器执行查询之前调用,传递的参数为查询器引用。用来在查询之前,绑定特殊的查询参数。
  • beforeList(&data):该接口在List查询器执行之后,渲染List视图之前调用。传递的参数为视图参数引用,其中包括查询出的model集合。用来对查询的model 集合做后处理,或者对list视图绑定一些Module专有的参数。
  • beforeEditExcuteQuery(&querier):该接口在Edit请求中Model查询器执行查询之前调用,传递的是查询器引用。用来绑定查询model需要的特殊参数。
  • beforeEdit(&data):该接口在Edit中Model查询器执行之后,渲染视图之前调用,传递的是视图参数引用,其中包括查询器查询出的model。用来做渲染前的预处理。
  • afterSave(&model):该接口在Edit中,保存编辑的之后调用,传递的是保存在数据库中,最新的数据库记录持久化的model。用来对model做一些复杂的后级联处理。
  • beforeView(data): 该接口在View请求中,View 查询器查询之后调用,传递的是视图参数的引用。用来对视图显示做预处理。

2)        Model 接口

GModule MVC代码中的Model也继承自BaseModel,实现 BaseModel类开放的一些接口可以完成扩展。

formatXXXAttribute():该接口用来格式化某个字段。本产品基于Laravel,其已经具备类似的接口,就是getXXXXAttribute()。但这样的接口的优先级比字段优先级高,这在特殊的情况下为开发带来了不便,所以再设计一个类似的接口,该接口的优先级低于字段本身。

3)        View 接口

视图的扩展接口与前两者不同,主要体现在子视图与视图块上,也就是在Core CURD模块的视图基础上,扩展视图组件。默认Core CRUD MVC视图生成的是一个表格或者一个表单,占满页面。而View接口将提供在该表格上下左右扩展页面组件的能力。

4)        Configuration

每一个GModule对应一个Configuration文件,其中包含GModule对主表各个字段的配置参数,以及布局参数。

3.1.3 模块关系

CRUD请求路由到GModule的Controller,GModule代码实现Core CRUD MVC开放的接口,而由Core CRUD Module去真正实现对数据库的CRUD操作。每一个GModule的信息应该被记录在数据库表中,以便给GModule关联菜单,控制权限,记录操作日志等等。一些主要模块之间的关系如下图所示。

图3-2模块关系

从图2-2中可以看到,由GModule管理模块根据用户配置来生成一个GModule A,当用户的CRUD请求到达GModule A时,GModule 会讲请求转交Core CRUD进行处理,Core CRUD 模块再以SQL对数据库进行CRUD操作。

3.1.4 Build 与 CRUD流程

DBuilder项目的方案,将真正的CRUD操作交给了Core CRUD Module去执行,CRUD参数由GET或者POST请求参数与GModule Configuration构成,而GModule的MVC代码只是去实现Core CRUD MVC开放的一些预处理或者后处理接口。

图2-3是DBuilder最核心的流程图,包含Module的生成和处理CRUD请求的过程,图2-4是SximoBuilder 中Module的生成和处理CRUD请求的流程图。

图3-3 DBuilder 代码生成和处理CRUD的流程

图3-4 SximoBuilder 代码生成和处理CRUD的流程

对比两者,可以看到两者的最大区别,是DBuilder复用一份CRUD代码,而不是像Sximo那样为每一个Module生成一套可以当独执行的CRUD代码。这样做的好处是提高了复用性,并通过Module CRUD MVC实现预处理/后处理接口达到扩展性的目的。

3.2 Core数据源

Core数据源是DBuilder的默认数据源,其类型为mysql,数据库名为dbuilder,本节按照《数据原型分析》一节进行详细的数据库设计。为提高程序性能,数据源信息保存在代码文件app/config/datasource.php中,文件内容如下:

<?php return array (

'core' =>

array (

'driver' => 'mysql',

'host' => 'localhost',

'database' => 'dbuilder',

'username' => 'root',

'password' => 'root',

'charset' => 'utf8',

'collation' => 'utf8_unicode_ci',

'prefix' => '',

'edit' => false,

'port' => 3306,

),

// more data source

);

其中Core数据源有下述数据表:

1)        d_menu 表:表示后台左侧树形菜单,每一个可点击跳转的菜单项必须与一个Module进行关联。

表3-1 web后台左侧菜单表

field

type

default

info

id

int

auto_increment

PRI

module_id

int

module_name

varchar(12)

parent_id

null

父菜单项

title

varchar(12)

module_title

显示名称

_order

int

0

排序字段

2)        d_module 表:记录了module信息,每一条d_module表的记录代表了DBuilder生成的一个Module。

表3-2 module信息描述

field

type

default

info

id

int

auto_increment

PRI

name

varchar(32)

UIN

title

varchar(32)

module标题

note

varchar(32)

module 说明

db_source

varchar(16)

core

数据源名称

db_table

varchar(16)

module主表

db_table_key

varchar(16)

主表PRI

3)        d_user 表:保存着使用后台程序的用户。

表3-3 web后台用户

field

type

default

info

id

int

auto_increment

PRI

username

varhcar(64)

用户名

email

varchar(64)

邮箱

password

varchar(64)

HASH

salt

varchar(64)

last_login

timestamp

最后登录时间

remember_token

记住密码口令

group_id

int

组ID

4)        d_group表:表示对后台用户的分组信息。

表3-4 用户分组表

field

type

default

info

id

int

auto_increment

PRI

name

varhcar(64)

组名

note

varchar(128)

组说明

level

int

组级别

5)        d_group_access表:记录了每个GModule、不同后台用户组与各种操作权限的三维权限信息。

表3-5 用户组对Module权限表

field

type

default

info

id

int

auto_increment

PRI

group_id

int

组id

module_id

int

Module模块ID

edit

int

1

可编辑

view

int

1

可查看

delete

int

1

可删除

export

int

1

可导出

6)        d_log表:记录了每个用户的操作日志。

表3-6 用户操作日志

field

type

default

info

id

int

auto_increment

PRI

user_id

int

用户id

ip_addr

varchar(15)

客户端IP

module_id

int

访问的moduleid

module_title

varchar(16)

task

varchar(16)

操作

created_at

timestamp

可导出

3.3 数据源管理模块

DBuilder需要支持多数据源,多种类型数据库。数据源信息保存在d_database表中。考虑到数据库操作是频繁操作,如果将数据源信息保存在数据库中,则每次数据库操作将多一次数据源查询操作,这样做浪费性能。那么DBuilder不应该把数据源信息保存在数据库中,而应该保存在代码文件中。数据源管理的信息包括数据源名称(数据源的唯一标识,DBuilder默认的数据源名为core)、数据库类型、地址、端口、数据库名、用户名、密码等等信息。因为数据源管理模块并不对表进行增删改查操作,所以数据源管理模块并不是一个GModule模块。该模块的代码完全手工编写。

3.4 GModule 管理模块

DBuilder将以基于名字为“Module”的GModule作为生成GModule的用户接口,该模块称作GModule管理模块,换言之GModule管理模块本身就是一个GModule,该GModule的主表即是core数据源中保存GModule信息的数据库表,改GModule的名字为“Module”。GModule 管理模块包含创建,更新和删除GModule 的所有代码文件以及数据库记录。GModule的新建和删除需要更新全局的GModule路由。

1)        GModule 路由

GModule路由定义在一个独立的代码文件中,为一个以GModule名字进行减号分词并全部小写的字符串为键(譬如:GModule名字为OrderItem,则键值为order-item)、以Module中Controller类的类名为值的map字典,GModule路由是全局的。

2)        GModule 新建&更新

新建GModule将在数据库中生成一条记录、生成所有的module文件、并更新路由。更新操作只修改配置文件。新建与更新都使用相同的编辑视图,此编辑视图是对GModule Configuration的图形化配置界面。

3)        GModule 删除

GModule删除将删除所有的GModule MVC代码,删除GModule Configuration代码,删除数据库表记录,并更新GModule路由。

3.5 Core CRUD 模块

Core CRUD 模块是DBuilder处理CRUD请求的实际处理者,它由下述几部分组成:

1)        参数解析初始化

初始化Model,实例化一个Module的Model对象作为初始化查询器。加载Module Configuration,对未设置的值进行设置默认值,对参数进行汇聚。

2)        表单Form

主要包括新建和更新功能。根据GModule主表主键primaryKey是否设置判断是新建还是更新操作。下图是Form模块的流程

图3-5 Form执行流程

Form 分两部分,第一部分渲染Form页面给用户填写。第二部分为Form保存。

渲染Form页面需要考虑的有Form控件和有外键关系的字段要怎么处理。Form控件需要支持类型包括text、text_date、text_datetime、textarea、select、radio、checkbox、file、hidden、address以及custom,自定义控件应该继承FormControl类,自定义控件的渲染由控件的render方法完成。Form渲染需要判断有关系的字段做辅助加载。比如对post(文章)表进行编辑,post表有一个字段为category_id,表示文章的栏目ID,对应category(栏目)表的id字段。这时需要对category_id使用select,radio,checkbox控件进行加载,方便用户输入。比如使用select控件,那么应该将category.id作为option的value,将category.name作为option中的text。这样做也是为了方便用户输入。此步骤与List中搜索时有共性,因此代码可复用。

Form 保存需要考虑一些自定义控件的保存,自定义控件的数保存由自定义控件类的onSave方法完成。Form 保存还需要考虑关系的保存,默认应该级联更新附属表。Form 表单在用户输入完成点击保存之后,要分下面几步:

  • 根据字段配置的验证规则进行验证;
  • 应判断Module Configuration 中的relation进行分析,进行必要的级联操作;
  • 并要调用自定义控件的onSave方法;
  • 最后才应更新或新建主表数据;
  • 跳转:更新或新建成功跳转至List,失败跳转至Form。

Form 还需要开放对应的预处理和后处理接口。

3)        列表List(Table)

List是一个分页Table,按照Module Configuration 中的字段配置显示分页数据。支持列表搜索,排序,勾选删除,导出等功能;

  • 分页展现数据以InitQuerier模块得到的Model作为查询器,结合分页,查询出基本的数据列表。分页类型为全页刷新类型(非异步分页);
  • List搜索:支持在Module Configuration中定义了search不等于false的字段作为搜索条件。搜索关系为逻辑与的关系。并反映在GET参数上。搜索输入控件根据字段的form type来定。在Form 中定义为select,radio,checkbox控件的字段,在List中都将使用select控件作为输入控件;
  • List 排序:以在Module Configuration中定义了form.sort 不等于 false的字段作为可排序字段。排序只支持按单一字段排序,降序方式含升序和降序;
  • List 多选操作主要支持多选删除,多选复制操作,任何删除操作都需确认;
  • List 数据每行记录的支持的操作按Module Configuration中的配置给出,默认支持编辑,删除,查看三项操作;
  • List 也要开放预处理/后处理接口给Module CRUD MVC。

4)        查看View

View 暂时以Form为基础,提供预处理后处理接口,但不允许编辑。

第四章           DBuilder系统实现

4.1 目录结构

代码按照前段资源、MVC、Configuration、Library等概念进行了分目录存放。下面表格中给出了主要目录的说明:

表4-2 代码主要目录

目录

作用

assets

此目录存放着各种各样的前端资源。包括bootstrap,以及自定义的css和js文件。

plugins

存放特殊前端插件的目录,比如富文本编辑器,视音频插件等等。

app/controllers/admin

存放着MVC中控制器的目录。其中,DBuilder的核心在admin目录下。

app/models

存放着MVC中模型(Model)的目录。用来做数据库查询用。

app/views

存放着MVC中视图的目录。文件名以*.blade.php的格式命名。

app/library

存放PHP辅助类,PHP库的目录。

app/config/crud

存放Module Configuration的目录。

4.2 GModule 配置文件

GModule配置文件定义了GModule的参数,该文件保存在app/config/crud/下,是以GModule Name进行蛇形分词得到的字符串命名的php文件(譬如:一GModule的名字为OrderItem,则GModule配置文件为order_item.php)。配置参数以数组格式返回。

考虑到PHP数组在表格中呈现的美观性,对参数以配置中的Key=>Value形式,以点分形式Key.Value表示。

表4-3 root配置

Configuration Key

类型

默认值

含义

fields

array

array()

字段列表

fields.field_name

array

array()

对field_name字段的配置

fields.field_name.label

string

UP(field_name)

显示在列表表格的表头的内容,和form控件旁边的内容

fields.field_name.form

array

array()

field_name字段的表单配置,具体参考

fields.field_name.form配置

fields.field_name.list

array

array()

field_name 字段的列表配置,具体参考

fields.field_name.list配置

fields.field_name. relation

array

array()

field_name 字段的关系

表4-3中每个字段的表单配置说明如下表所示:

表4-4 fields.field_name.form配置

Configuration Key

值类型

默认

含义

type

string

text

Form控件类型

show

bool

true

是否出现在表单

hidden

bool

false

是否以隐藏的空间在表单中

rule

string

required

验证规则

ajax_validate

bool

false

是否异步验证

placeholder

string

控件中的提示

表4-3中每个字段的列表配置说明如下表所示:

表4-5 fields.field_name.list配置

Configuration Key

值类型

默认

含义

show

bool

true

是否出现在表单

sort

bool

true

字段是否可以排序,默认可排序

search

bool,array

array()

是否可搜索以及搜索规则

string

控件中的提示

表4-3中每个字段的关系配置说明如下表所示:

表4-6 fields.field_name.relation配置

Configuration Key

值类型

默认

含义

table

string

关联表

foreign_key

string

id

对应关联表里的字段

show

string

关联表里的一个字段,当需要转义时,将用该字段代替field_nae字段显示

as

string

table_show

转义查询出的值用哪个字段表示,主要为了防止主表和关联表有重复字段

下面是一个名为post的GModule的Configuration文件

 <?php return array (
'data_source' => 'core',
'table' => 'post',
'fields' =>
array (
'id' =>
array (
'label' => 'ID',
'form' =>
array (
'show' => true,
'hidden' => true,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'title' =>
array (
'label' => '标题',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'short' =>
array (
'label' => '摘要',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'textarea',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'id',
'as' => '',
),
),
'content' =>
array (
'label' => '正文',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'wysiwyg',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => false,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'id',
'as' => '',
),
),
'view_ct' =>
array (
'label' => '查看次数',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'created_at' =>
array (
'label' => '创建时间',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => false,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'updated_at' =>
array (
'label' => '更新时间',
'form' =>
array (
'show' => false,
'hidden' => false,
'type' => 'text',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
'category_id' =>
array (
'label' => '栏目',
'form' =>
array (
'show' => true,
'hidden' => false,
'type' => 'select',
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => '',
),
'list' =>
array (
'show' => true,
'sort' => true,
'search' => '=',
'lookup' => false,
),
'relation' =>
array (
'type' => 'belongsTo',
'table' => 'category',
'foreign_key' => 'id',
'show' => 'title',
'as' => 'category_title',
),
),
),
'list_options' =>
array (
'page' => 10,
'create' => true,
'update' => true,
'delete' => true,
),
'form_options' =>
array (
'layout' =>
array (
'cols' => 12,
'label_cols' => 1,
'input_cols' => 11,
),
),
'relations' =>
array (
'id' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'title' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'short' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'content' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'view_ct' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'created_at' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'updated_at' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
'category_id' =>
array (
'type' => '',
'table' => '',
'foreign_key' => '',
'show' => '',
'as' => '',
),
),
);

GModule Configuration

4.3 数据源管理模块实现

数据源管理模块完成基于网页界面对app/config/datasource.php文件的配置。包含数据源列表页,数据源新建与编辑页。

  • 数据源列表页:对应请求路径为admin/data-source/list,此页面读取DataSource文件渲染List(table)页面,密码不显示。
  • 数据源编辑页面:对应请求路径为 admin/data-source/edit,GET请求读取DataSource问价内容渲染表单,POST请求将数据源参数输出值DataSource文件。此页面提供测试连接功能,实现方法为基于表单的数据源参数连接数据库,执行一条SQL语句,连接是否成功取决于SQL是否执行成功。

实现数据源管理的核心控制器代码放在DataSourceController.php文件中。

 <?php
/**
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/12
* Time: 15:35
*/ namespace admin; use BaseModel;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Redirect;
use SiteHelpers;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Input; use PDOException;
use PDO;
class DataSourceController extends AdminController
{
/**
* 呈现数据源列表
*/
public function getList()
{
$datasources = SiteHelpers::loadDataSources();
$this->makeView(array(
'datasources' => $datasources,
));
} /**
* 异步加载某数据源的所有数据表
* @return \Illuminate\Http\JsonResponse
*/
public function getTables()
{
$dataSourceName = Input::get("data_source");
$dataSources = SiteHelpers::loadDataSources(); $dataSource = $dataSources[$dataSourceName];
$tables = BaseModel::getTableList($dataSource['database'], $dataSourceName);
return Response::json(array(
'success' => true,
'data' => array(
'tables' => $tables,
'selected' => Input::get('table'),
),
));
} /**
* 呈现数据源编辑或者新建FORM
* @param null $slug
*/
public function getEdit($slug = null)
{
$dataSource = null;
if ($slug) {
// 更新
$dataSource = $slug === 'core' ? Config::get('database.connections.core')
: Config::get('datasource.' . $slug);
$dataSource['name'] = $slug;
} else {
// 新建
$dataSource = array(
'name' => '',
'driver' => 'mysql',
'host' => 'localhost',
'port' => 3306,
'database' => '',
'username' => 'root',
'password' => '',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci'
);
} $this->makeView(array(
'dataSource' => $dataSource
));
} /**
* 测试数据源连接是否可靠
* @return \Illuminate\Http\JsonResponse
*/
public function postTest(){
$success = true;
try{
$dsn = Input::get('driver').':'.Input::get('host').':'.Input::get('port').';dbname='.Input::get('database');
$dbh = new \PDO($dsn,Input::get('username'),Input::get('password'));
// $connection = new Connection($dbh,Input::get('database'));
// $key = md5(date("Y-m-d H:i:s"));
// DB::addConnection($key,$connection);
// if(!DB::connection($key)->getDatabaseName()){
// $success = false;
// }
$dbh = null;
}catch(PDOException $e){
$success = false;
}
return Response::json(array(
'success' => $success,
));
} /**
* 保存编辑好的数据源信息
* @param null $primaryKeyValue
* @return mixed
*/
public function postEdit($primaryKeyValue = null)
{
$dataSources = SiteHelpers::loadDataSources();
$name = Input::get('name');
$dataSources[$name] = Input::all();
SiteHelpers::saveDataSources($dataSources); return Redirect::action(get_class($this).'@getList');
} public function getTableFields(){
$connection = Input::get('connection');
$table = Input::get('table'); $rawFields = BaseModel::getTableColumns($table,$connection); $fields = array();
$pri = null;
foreach($rawFields as $field){
if($field->Key === 'PRI'){
$pri = $field->Field;
}
$fields [] = $field->Field;
} return Response::json(array(
'success' => true,
'data' => array(
'fields' => $fields,
'pri' => $pri,
),
));
}
}

DataSourceController

4.4 CoreCRUD 模块实现

CoreCRUD模块涉及的代码文件极其作用如下说明。

  • app/controllers/admin/AdminController.php:CoreCRUD模块的控制器,是CRUD操作核心的逻辑代码。主要分析请求参数和Module参数,调用Model层,渲染视图层,实现List呈现、List搜索排序、Form呈现、Form保存等功能;
  • app/models/BaseModel.php:CoreCRUD模块中的模型,是ModuleCRUD模块中模型的基类。未与表格关联。定义了一些公共的Model默认属性,以及一些静态的数据库操作方法,比如拉取数据库表字段列表;
  • app/config/crud/admin.php:CoreCRUD模块中的默认crud参数配置文件,但ModuleCRUD模块中的配置文件未定义某些参数时,将使用admin.php中的默认参数;
  • app/views/admin/core/list.blade.php:CoreCRUD模块中的列表视图文件,用来呈现数据列表;
  • app/views/admin/core/form.blade.php:CoreCRUD模块中的数据记录编辑视图文件,用来呈现数据编辑的表单。

代码文件如下

 <?php

 namespace admin;

 use Illuminate\Support\Facades\Request;
use Illuminate\Support\Facades\Response;
use Illuminate\Support\Facades\Redirect;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\View;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\URL;
use SiteHelpers;
use Module; class AdminController extends \BaseController
{
protected $layout = 'layouts.admin.main'; /**
* AdminController constructor.
*/
public function __construct()
{
parent::__construct();
View::share('stdName',$this->getStdName());
View::share('reducName',SiteHelpers::reducCase($this->getStdName()));
View::share('routeParams',$this->getRouteParams());
if($this->model){
$this->assignModel($this->model);
}
View::share('config',$this->savedConfig);
if(!Cache::has('modules')){
Cache::forever('modules',Module::all());
}
} public function getList(){
$models = $this->paginateModels();
$view = $this->getRouteParam('c').'._list';
if(!View::exists($view)){
$view = 'admin.core.list';
}
if(Request::ajax() || Input::has('isAjax')){
return Response::json(array(
'success' => true,
'data' => array(
'models' => $models->toArray()
)
));
}else{
$this->makeView(array(
'models' => $models,
$this->getStdName().'s' => $models,
),$view);
}
} public function getEdit($id = null)
{
if($id){
$this->model = $this->model->find($id);
}
$data = array(
$this->model->getKeyName() => $id,
'model' => $this->model,
$this->modelName=>$this->model,
);
$this->beforeEdit($data); $view = $this->getRouteParam('c').'._form';
if(!View::exists($view)){
$view = 'admin.core.form';
}
$this->makeView($data,$view);
} protected function config(){
$config = 'crud/'.$this->getStdName();
if(!file_exists(app_path('config/').$config.'.php')){
$config = 'crud/admin';
}
return Config::get($config);
} protected function assignModel($model)
{
$this->model = $model; $config = $this->config();
$defaultConfig = Config::get('crud/admin');
$relations = array();
/* 将默认参数传递给module config */
foreach($config['fields'] as $field => &$fieldConfig){
// if(isset($fieldConfig['value'])){
// $this->model->$field = $fieldConfig['value'];
// }
$fieldConfig['form'] = array_merge(
$defaultConfig['fields']['field_name']['form'],
isset($fieldConfig['form']) ? $fieldConfig['form'] : array()
);
$fieldConfig['list'] = array_merge(
$defaultConfig['fields']['field_name']['list'],
isset($fieldConfig['list']) ? $fieldConfig['list'] : array()
);
if(isset($fieldConfig['relation']) &&
isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] !== '' ){
$relations[$field] = $fieldConfig['relation'];
}
} $config['list_options'] = array_merge(
$defaultConfig['list_options'],
isset($config['list_options']) ? $config['list_options'] : array()
); $config['form_options'] = array_merge(
$defaultConfig['form_options'],
isset($config['form_options']) ? $config['form_options'] : array()
); /* 将字段的relation汇聚出来,是为了后面的代码方便,同时减少循环 */
$config['relations'] = $relations; $this->savedConfig = $config;
} protected function paginateModels()
{
$models = array();
if($this->model){
$query = $this->model->newQuery();
$this->handleListQuery($query);
$selects = array($this->model->getTable().'.*'); foreach($this->savedConfig['relations'] as $field=>&$params){
$query->join($params['table'],$params['table'].'.'.$params['foreign_key'],'=',$this->model->getTable().'.'.$field);
if(!isset($params['as'])){
$params['as'] = $params['table'].'_'.$params['show'];
}
$selects[] = $params['table'].'.'.$params['show'] . ' as '.$params['as'];
}
$query->select($selects);
$orderBy = Input::get('list_order_by');
if( $orderBy){
$query->orderBy($this->model->getTable().'.'.$orderBy,Input::get('list_sort_asc') ? 'asc' : 'desc');
}else{
$query->orderBy($this->model->getTable().'.'.$this->model->getKeyName(),'desc');
}
$page = Input::has('_page') ? Input::get('_page') : 10;
$models = $query->paginate($page);
}
return $models;
} public function postEdit($primaryKeyValue = null){ $primaryKeyName = $this->model->getKeyName();
if($primaryKeyValue == null){
$primaryKeyValue = Input::get($primaryKeyName);
} $fields = $this->savedConfig['fields'];
$datas = array();
foreach($fields as $field => $fieldConfig){
if(Input::has($field)){
$datas[$field] = Input::get($field);
}
} if($primaryKeyValue){
$this->model->where($primaryKeyName,$primaryKeyValue)->update($datas);
}else{
$this->model->fill($datas);
$this->model->save();
}
$this->afterSave($this->model);
$resp = Redirect::action(get_class($this).'@getList')->withMessage('save success!'); return $resp;
} public function getIndex()
{
$this->makeView(null,'admin.index');
} public function postDelete(){
$ids = explode(',',Input::get('ids'));
$data = array();
$success = true;
$data['ids'] = $ids;
$ids = array_filter($ids,function($id){
return $id;
});
$this->model->whereIn($this->model->getKeyName(),$ids)->delete();
$data['redirect_url'] = URL::to(action(get_class($this).'@getList'));
return Response::json(array(
'success' => $success,
'data' => $data,
));
} public function getDelete($id){
$this->beforeDelete($id);
$this->model->where($this->model->getKeyName(),$id)->delete();
return Redirect::action(get_class($this).'@getList');
} public function missingMethod($parameters = array())
{
//
$this->makeView(null,'site.404');
} protected function handleListQuery(&$query)
{
$searchFields = array_intersect_key($this->savedConfig['fields'],Input::all());
foreach($searchFields as $field=> $fieldConfig){
if(isset($fieldConfig['list']['search'])){
$value = Input::get($field);
$operator = $fieldConfig['list']['search'];
if($value !== ''){
if($operator){
if($operator === 'like'){
$value = '%'.$value.'%';
}
$query = $query->where($this->model->getTable().'.'.$field,$operator,$value);
}else{
$query = $query->where($this->model->getTable().'.'.$field,$value);
}
}
}
} } protected function beforeDelete($id)
{ } protected function beforeEdit(&$data)
{ } protected function afterSave($model)
{ } public function getHelp(){
$this->makeView(null,'admin.help');
} }

AdminController

 <?php

 /**
* Created by PhpStorm.
* User: Administrator
* Date: 2015/10/10 0010
* Time: 17:58
*/
class BaseModel extends Eloquent
{
protected $table = '';
protected $guarded = array('id');
public $timestamps = false;
public static function getTranslates($translate){
$rows = DB::table($translate['table'])->select(array($translate['foreign_key'],$translate['show']))->get();
return $rows;
} static function getTableList( $db ,$connection = null)
{
$t = array();
$dbname = 'Tables_in_'.$db ;
$tables = $connection ? DB::connection($connection)->select("SHOW TABLES FROM {$db}") : DB::select("SHOW TABLES FROM {$db}");
foreach($tables as $table)
{
$t[$table->$dbname] = $table->$dbname;
}
return $t;
} static function getTableColumns( $table,$connection = false)
{
// $columns = array();
$sql = "SHOW COLUMNS FROM $table";
$rawColumns = $connection ? DB::connection($connection)->select($sql)
: DB::select($sql);
// foreach($rawColumns as $column)
// $columns[$column->Field] = $column->Field;
return $rawColumns;
} function getColoumnInfo( $result )
{
$pdo = DB::getPdo();
$res = $pdo->query($result);
$i =0; $coll=array();
while ($i < $res->columnCount())
{
$info = $res->getColumnMeta($i);
$coll[] = $info;
$i++;
}
return $coll; } function builColumnInfo( $statement )
{
$driver = Config::get('database.default');
$database = Config::get('database.connections');
$db = $database[$driver]['database'];
$dbuser = $database[$driver]['username'];
$dbpass = $database[$driver]['password'];
$dbhost = $database[$driver]['host']; $data = array();
$mysqli = new mysqli($dbhost,$dbuser,$dbpass,$db);
if ($result = $mysqli->query($statement)) { /* Get field information for all columns */
while ($finfo = $result->fetch_field()) {
$data[] = (object) array(
'Field' => $finfo->name,
'Table' => $finfo->table,
'Type' => $finfo->type
);
}
$result->close();
} $mysqli->close();
return $data; } static function findPrimarykey( $table, $db = null)
{
$query = "SHOW KEYS FROM `{$table}` WHERE Key_name = 'PRIMARY'";
$primaryKey = '';
$keys = $db ? DB::connection($db)->select($query) : DB::select($query); foreach($keys as $key)
{
$primaryKey = $key->Column_name;
} return $primaryKey;
}
}

BaseModel

 <?php
$formOption = $config['form_options'];
$layout = $formOption['layout'];
$labelCols = $layout['label_cols'];
$inputCols = $layout['input_cols'];
$labelCss = "col-sm-$labelCols";
$inputCss = "col-sm-$inputCols";
// 插件是否加载
$loadUE = false;
$loadSBox = false;
$loadDatePicker = false;
?>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">@if($model->id) 编辑<code>#{{$model->id}}</code>@else 新建 @endif</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-{{$layout['cols']}} col-sm-offset-{{(12-$layout['cols'])/2}}">
<form class="form-horizontal validate" action="{{URL::to('admin/'.$stdName.'/edit')}}" method="post">
<input type="hidden" name="{{$model->getKeyName()}}" value="{{$model->getKey()}}">
<?php foreach($config['fields'] as $field => $settings):?>
<?php
if ($field === $model->getKeyName() || !$settings['form']['show']) continue;
$type = $settings['form']['type'];
$rule = $settings['form']['rule'];
?>
<?php if($settings['form']['type'] === 'hidden'):?>
<input type="hidden" name="{{$field}}" value="{{$model->$field}}">
<?php continue;?>
<?php endif; ?>
<div class="form-group">
<label for="{{$field}}"
class="{{$labelCss}} control-label">{{isset($settings['label']) ? $settings['label'] : strtoupper($field)}}</label>
<div class="{{$inputCss}}">
<?php if($type === 'textarea'):?>
<textarea name="{{$field}}" id="{{$field}}" rows="10"
{{SiteHelpers::inputValidate($rule)}}
class="form-control">{{$model->$field}}</textarea>
<?php elseif($type === 'select'):?>
<?php $loadSBox = true;?>
<select name="{{$field}}" id="{{$field}}" class="selectboxit" {{SiteHelpers::inputValidate($rule)}}>
@if (isset($settings['form']['options']))
@if(is_array($settings['form']['options']))
@foreach($settings['form']['options'] as $value => $text)
<option value="{{$value}}"
@if($value == $model->$field) selected @endif>{{$text}}</option>
@endforeach
@elseif(is_string($settings['form']['options']))
@foreach($$settings['form']['options'] as $value => $text)
<option value="{{$value}}">{{$value}}</option>
@endforeach
@endif
@elseif(isset($settings['relation']['type']) && $settings['relation']['type'] )
<?php
$fieldTranslate = $config['relations'][$field];
$options = BaseModel::getTranslates($fieldTranslate);
foreach($options as $option):
?>
<option value="<?=$option->$fieldTranslate['foreign_key']?>"
@if($option->$fieldTranslate['foreign_key'] == $model->$field) selected @endif
><?=$option->$fieldTranslate['show']?>
</option>
<?php endforeach;?>
@endif
</select>
<?php elseif($type === 'wysiwyg'):?>
<?php
$loadUE = true;
?>
<script type="text/plain" name="{{$field}}" id="wysiwyg-edit"
style="width:100%;height:240px;">{{$model->$field}}</script>
<?php elseif ($type === 'radio' || $type === 'checkbox'): ?> @if(isset($settings['form']['options']))
@foreach($settings['form']['options'] as $option => $text)
<div class="{{$type}} {{$type}}-replace">
<input type="{{$type}}" value="{{$option}}" name="{{$field}}"
id="{{$field}}" @if($model->field === $option) checked @endif>
<label>{{$text}}</label>
</div>
@endforeach
@endif
<?php elseif($type === 'date'):?>
<?php $loadDatePicker = true;?>
<div class="input-group">
<input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" {{SiteHelpers::inputValidate($rule)}}>
<div class="input-group-addon">
<a href="#"><i class="entypo-calendar"></i></a>
</div>
</div>
<?php elseif($type === 'password'):?>
<input type="password" class="form-control" name="{{$field}}" id="{{$field}}"
value="{{$model->$field}}"
{{SiteHelpers::inputValidate($rule)}}
>
<?php elseif($type === 'file'):?>
<input type="file"
class="form-control file2 inline btn btn-primary"
data-label="<i class='glyphicon glyphicon-file'></i> 选择文件" >
<?php else:?>
<input type="text" class="form-control" name="{{$field}}" id="{{$field}}"
{{SiteHelpers::inputMask($rule)}}
{{SiteHelpers::inputValidate($rule)}}
value="{{$model->$field}}">
<?php endif;?>
</div>
</div>
<?php endforeach;?>
<div class="form-group">
<div class="{{$inputCss}} col-sm-offset-{{$labelCols}}">
<button type="submit" class="btn btn-primary">保存</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
@yield('form.bottom','')
@section('styles')
@if($loadSBox)
{{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}}
@endif
@append
@section('scripts')
<?php if($loadUE):?>
{{HTML::script('plugins/ue-utf8-php/ueditor.config.js')}}
{{HTML::script('plugins/ue-utf8-php/ueditor.all.min.js')}}
{{HTML::script('plugins/ue-utf8-php/lang/zh-cn/zh-cn.js')}}
<?php endif;?>
@if($loadSBox)
{{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}}
@endif
@if($loadDatePicker)
{{HTML::script('assets/js/bootstrap-datepicker.js')}}
@endif
{{HTML::script('assets/js/jquery.inputmask.bundle.min.js')}}
{{HTML::script('assets/js/jquery.validate.min.js')}}
@append @section('footScript')
<script>
var ue = null,
ueId = 'wysiwyg-edit';
if (document.getElementById(ueId)) {
ue = UE.getEditor(ueId);
}
</script>
@append

app/views/admin/core/form.blade.php

 @section('headStyle')
<style>
.sort.sort-active {
color: #000;
font-weight: bold;
}
</style>
@append
<?php
$list_options = $config['list_options'];
$loadSBox = false;
$loadDatePicker = false;
?>
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title"><?=isset($navMap[$stdName]['text']) ? $navMap[$stdName]['text'] : strtoupper($stdName)?>
列表</h3>
</div>
<div class="panel-body">
<div class="row">
<div class="col-sm-12">
<div class="btn-group btn-group-sm" role="group">
@if($list_options['create'])
<a href="{{URL::to('admin/'.$stdName.'/edit')}}" class="btn btn-primary">新建</a>
@endif
<a class="btn btn-danger delete-selected">删除</a>
<a class="btn btn-default">导出</a>
</div>
</div>
</div>
<br>
<form class="list-form" action="" method="get">
<input type="hidden" name="list_sort_asc" value="{{Input::get('list_sort_asc') !== null ? Input::get('list_sort_asc') : 1}}">
<input type="hidden" name="list_order_by" value="">
<table class="table table-bordered responsive table-hover table-striped">
<thead>
<tr>
<th>
<div class="checkbox checkbox-replace">
<input type="checkbox" class="item-all">
</div>
</th>
<?php foreach($config['fields'] as $field=>$settings):?>
<?php if($settings['list']['show']):?>
<th @if($settings['list']['sort'])
class="sort @if($field === Input::get('list_order_by')) sort-active @endif"
data-field="{{$field}}" @endif
><?=is_array($settings) && isset($settings['label']) ? $settings['label'] : strtoupper($field)?>
<span class="pull-right">
@if($field === Input::get('list_order_by')) @if(Input::get('list_sort_asc') == 1) <i class="fa fa-sort-asc"></i> @else <i class="fa fa-sort-desc"></i> @endif @endif
</span>
</th>
<?php endif;?>
<?php endforeach;?>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr>
<td></td>
@foreach($config['fields'] as $field=>$fieldConfig)
@if($fieldConfig['list']['show'])
@if(isset($fieldConfig['list']['search']) && $fieldConfig['list']['search'] !== false)
<td>
@if($fieldConfig['form']['type'] == 'select' || ($fieldConfig['form']['type'] === 'radio' || $fieldConfig['form']['type'] == 'checkbox'))
<?php $loadSBox = true;?>
@if(isset($fieldConfig['form']['options']) && $fieldConfig['form']['options'])
<select name="{{$field}}" id="{{$field}}" class="selectboxit">
<option value="" class="default-value">请选择</option>
@foreach($fieldConfig['form']['options'] as $option => $text)
<option value="{{$option}}" @if(Input::get($field) && Input::get($field) === $option) selected @endif>{{$text}}</option>
@endforeach
</select>
@elseif(isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] )
{{View::make('components.relation_select',array(
'fieldConfig'=>$fieldConfig,'field' => $field, ))}}
@endif
@elseif($fieldConfig['form']['type'] === 'date')
<?php $loadDatePicker = true;?>
<input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" value="{{Input::get($field)}}">
@else
<input type="text" name="{{$field}}" id="{{$field}}"
value="{{Input::get($field)}}" class="form-control input-sm">
@endif
</td>
@else
<td></td>
@endif
@endif
@endforeach
<td>
<div class="btn-group btn-group-sm" role="group">
<button type="submit" class="btn btn-primary">搜索</button>
<button type="reset" onclick="resetForm(this)" class="btn btn-warning hidden">重置</button>
</div>
</td>
</tr>
<?php foreach($models as $model):?>
<tr>
<td width="18px">
<div class="checkbox checkbox-replace">
<input type="checkbox" name="d_delete_select" class="item" value="{{$model->id}}">
</div>
</td>
<?php foreach($config['fields'] as $filed=>$settings):?>
<?php if($settings['list']['show']):?>
<?php
$value = $model->$filed;
/* 字段在列表中需要翻译 */
if (array_key_exists($filed, $config['relations'])) {
$value = $model->$config['relations'][$filed]['as'];
}
?>
<td>{{$value}}</td>
<?php endif;?>
<?php endforeach;?>
<td>
<div class="btn-group btn-group-sm" role="group">
@if($list_options['update'])
<a href="{{URL::to('admin/'.$stdName.'/edit/'.$model->id)}}"
class="btn btn-primary">编辑</a>
@endif
@if($list_options['delete'])
<a href="{{URL::to('admin/'.$stdName.'/delete/'.$model->id)}}"
class="btn btn-danger">删除</a>
@endif
@if(View::exists('admin.'.snake_case($stdName).'.list_item_links'))
@include('admin.'.snake_case($stdName).'.list_item_links',array('model'=>$model))
@endif
</div>
</td>
</tr>
<?php endforeach;?>
</tbody>
</table>
</form>
<div class="pull-right">
{{$models->appends(Input::all())->links()}}
</div>
</div>
</div> @section('styles')
{{HTML::style('assets/js/datatables/responsive/css/datatables.responsive.css')}} @if($loadSBox)
{{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}}
@endif
@append @section('scripts')
{{HTML::script('assets/js/jquery.dataTables.min.js')}}
{{HTML::script('assets/js/datatables/jquery.dataTables.columnFilter.js')}}
@if($loadSBox)
{{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}}
@endif
@if($loadDatePicker)
{{HTML::script('assets/js/bootstrap-datepicker.js')}}
@endif
@append @section('footScript')
<script>
$(document).ready(function(){
$('th.sort').click(function(){
var $th = $(this);
$('input[name="list_order_by"]').val($th.data('field'));
$('input[name="list_sort_asc"]').val($th.find('i').hasClass('fa-sort-asc') ? 0 : 1);
$('form.list-form').submit();
}); $('input.item-all').change(function(){
var $this = $(this),
$items = $('input.item');
if($this.is(':checked')){
$items.prop('checked','checked');
}else{
$items.removeProp('checked');
}
$items.trigger('change');
}); $('a.delete-selected').click(function(){
var ids = [],
$items = $('input.item:checked');
$items.each(function(i){
ids.push($(this).val());
});
var idsStr = ids.join(',');
confirmModal({
message : '确认删除:'+idsStr,
onOk: function(){
$.post('{{URL::to('admin/'.snake_case($stdName).'/delete')}}',{"ids":idsStr},function(resp){
if(resp.success){
window.location.href = resp.data.redirect_url;
}
},'json');
}
});
return false;
});
});
</script>
@append @section('modals')
<div class="modal fade" id="confirm-modal" data-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">操作确认</h4>
</div>
<div class="modal-body"> </div>
<div class="modal-footer">
<button type="button" class="btn btn-default cancel" data-dismiss="modal">取消</button>
<button type="button" class="btn btn-info ok">确认</button>
</div>
</div>
</div>
</div>
@stop

app/views/admin/core/list.blade.php

 <?php
/**
* 说明:
* 1. 以下配置项,不设置便是默认
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/2
* Time: 12:33
*/ return array( /**
* 所有字段配置
*/
'fields' => array(
'field_name' => array(
/* 显示在列表表格的表头的内容,和form控件旁边的内容*/
'label' => '字段中文名',
/* 字段缺省值 */
'value' => false,
/* 针对表单的设置 */
'form' => array(
'show' => true,
'hidden' => false,
/*
* 字段对应表单的控件类型,默认text,
* 还支持常用的控件类型
* textarea
* radio
* checkbox
* number
* ipaddr
* wyswyg
* select
* date
* file
* 以及自定义类型
* */
'type' => 'text',
/*
'type' => array(
'select' => array(
'options' => function(){
return array();
}
),
),
'type' => array(
'radio' => array(),
),
*/
/* 提交表单后的验证规则 */
'rule' => 'required',
'ajax_validate' => false,
'placeholder' => 'xx', ),
// 针对列表的设置
'list' => array(
/* 字段在列表是否显示,默认为显示 */
'show' => true,
/* 字段是否可以排序,默认不能排序 */
'sort' => true,
/* 是否能够按这个字段搜索 */
'search' => true,
/* 字段进行翻译,比如栏目Id字段,一般要转成栏目名称显示 */
'lookup' => false,
), ),
// more fields
), /**
* 全局form配置,优先级小于字段配置
*/
'form_options' => array(
'layout' => array(
'cols' => 12,
'label_cols' => 1,
'input_cols' => 11,
),
), /**
* 全局list配置,优先级小于字段配置
*/
'list_options' => array(
'page' => 10,
'create' => true,
'update' => true,
'delete' => true,
), );

app/config/crud/admin.php

4.5 GModule 管理模块实现

GModule是一类由DBuilder生成的模块,它有一组模板定义在app/template目录下:

  • app/template/_form.tpl
  • app/template/_list.tpl
  • app/template/controller.tpl
  • app/template/model.tpl

前面设计中指出,GModule管理模块本身是一个名为“Module”,主表为d_module,且手工建立的GModule,故其代码组成也是符合GModule规范的,笔者编写的代码主要为扩展代码。GModule管理模块对应了下述代码文件:

  • app/controllers/admin/ModuleController.php:控制器(Controller)代码,其实现CoreCRUD模块的接口,以及扩展的url接口;
  • app/models/Module.php:GModule管理模块的模型;
  • app/views/admin/module/_form.blade.php: FORM视图代码,其在原有的CoreCRUD 模块的FORM表单下部,扩展了一组Tab,其中第一个Tab中显示了所有字段的详细配置,通过以上扩展就能实现在CoreCRUD生成的Form表单页面中对GModule进行配置;
  • app/views/admin/module/_list.blade.php: LIST视图代码;
  • app/views/admin/module/fields_config.blade.php:字段配置表格视图代码;
  • app/views/admin/module/list_item_links.blade.php:扩展链接视图代码;
  • app/config/crud/module.php:GModule Configuration文件。

下面贴上主要的代码文件ModuleController.php

 <?php
/**
* Created by PhpStorm.
* User: lvyahui
* Date: 2016/5/12
* Time: 15:28
*/ namespace admin; define('MODULE_ROUTES', json_encode(include(app_path() . '/module_routes.php'))); use Illuminate\Support\Facades\Redirect;
use SiteHelpers;
use BaseModel;
use Module;
use ConfigUtils;
use Illuminate\Support\Facades\Input;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Response; class ModuleController extends AdminController
{ protected function beforeEdit(&$data)
{
$data ['dataSources'] = SiteHelpers::loadDataSources();
if ($data['model']->id) {
$data ['moduleConf'] = ConfigUtils::get($data['model']->name);
}
} protected function afterSave($module)
{
/* 生成代码文件 */
$codes = array(
'moduleName' => $module->name,
'moduleTitle' => $module->title,
'tablePrimaryKey' => BaseModel::findPrimarykey($module->db_table, $module->db_source),
'moduleNote' => $module->note,
'date' => date('Y-m-d'),
'dbSource' => $module->db_source,
'dbTable' => $module->db_table,
);
$this->removeFiles($codes['moduleName']);
/* 生成默认module Configuration*/
$moduleConfs = $this->buildConfiguration($module->db_table, $module->db_source);
SiteHelpers::saveArrayToFile(app_path('config/crud/') . snake_case($codes['moduleName']) . '.php', $moduleConfs); $controller = file_get_contents(app_path('template') . '/controller.tpl');
$model = file_get_contents(app_path('template') . '/model.tpl');
$formView = file_get_contents(app_path('template') . '/_form.tpl');
$listView = file_get_contents(app_path('template') . '/_list.tpl');
$codes['timestamps'] = isset($moduleConfs['fields']['created_at']) && isset($moduleConfs['fields']['updated_at'])
? 'true' : 'false';
$buildController = SiteHelpers::blend($controller, $codes);
$buildModel = SiteHelpers::blend($model, $codes);
/* 生成 MVC 文件*/
file_put_contents(app_path() . "/controllers/admin/{$codes['moduleName']}Controller.php", $buildController);
file_put_contents(app_path() . "/models/{$codes['moduleName']}.php", $buildModel);
$viewPath = app_path('/views/admin/') . snake_case($codes['moduleName']);
if (!file_exists($viewPath)) mkdir($viewPath);
file_put_contents($viewPath . "/_form.blade.php", $formView);
file_put_contents($viewPath . "/_list.blade.php", $listView); /* 更新路由 */
$moduleRoutes = json_decode(MODULE_ROUTES, true); //require(app_path().'/module_routes.php');
if (is_array($moduleRoutes)) {
$moduleRoutes[SiteHelpers::reducCase($codes['moduleName'])] = 'admin\\' . "{$codes['moduleName']}Controller";
SiteHelpers::saveArrayToFile(app_path() . '/module_routes.php', $moduleRoutes);
}
} protected function beforeDelete($id)
{
$module = Module::find($id);
$moduleName = $module->name;
$this->removeFiles($moduleName);
} /**
* 删除GModule相关文件文件
* @param $moduleName
*/
public function removeFiles($moduleName)
{
$controller = app_path('admin/controllers') . "/{$moduleName}Controller.php";
if (file_exists($controller)) {
unlink($controller);
}
$model = app_path('models') . "/{$moduleName}.php";
if (file_exists($model)) {
unlink($model);
}
$moduleConf = app_path('config/crud/') . snake_case($moduleName) . '.php';
if (file_exists($moduleConf)) {
unlink($moduleConf);
} $viewPath = app_path('/views/admin/') . snake_case($moduleName);
$formFile = $viewPath . '/_form.blade.php';
$listFile = $viewPath . '/_list.blade.php';
if (file_exists($formFile)) unlink($formFile);
if (file_exists($listFile)) unlink($listFile);
} private function buildConfiguration($table, $connection)
{
$rawColumns = BaseModel::getTableColumns($table, $connection);
$fields = ConfigUtils::build($rawColumns);
return array(
'data_source' => $connection,
'table' => $table,
'fields' => $fields,
);
} /**
* 获取字段配置列表
* @return bool
*/
public function getFieldsConfig()
{
$filedsConfig = null;
if (Input::has('module_name')) {
$filedsConfig = ConfigUtils::get(Input::get('module_name'))['fields'];
} else {
$table = Input::get('table');
$connection = Input::get('connection'); $filedsConfig = $this->buildConfiguration($table, $connection)['fields'];
} $resp = $this->makeView(array(
'fieldsConfig' => $filedsConfig,
));
if ($resp) {
return $resp;
}
} /**
* 保存字段列表配置
* @return mixed
*/
public function postSaveFieldsConf()
{
$resp = Redirect::action(get_class($this) . '@getEdit', Input::get('id'));
$postFields = Input::get('fields');
$moduleName = Input::get('module_key');
$confKey = SiteHelpers::reducCase($moduleName);
$savedConfig = ConfigUtils::get($confKey);
foreach ($savedConfig['fields'] as $fieldName => &$savefield) {
$postField = $postFields[$fieldName];
$savefield['label'] = $postField['label'];
$savefield['form']['show'] = isset($postField['form']['show']);
$savefield['list']['show'] = isset($postField['list']['show']);
}
ConfigUtils::saveGModuleConf($confKey, $savedConfig);
return $resp;
} /**
* 呈现某一字段的配置参数FORM
* @return bool
*/
public function getFieldConfig()
{
$moduleKey = Input::get('module_key');
$field = Input::get('field'); $moduleConfig = Config::get('crud/' . snake_case($moduleKey));
$fieldConf = &$moduleConfig['fields'][$field];
$dbSource = SiteHelpers::loadDataSources()[$moduleConfig['data_source']];
$tables = BaseModel::getTableList($dbSource['database'],$moduleConfig['data_source']); $resp = $this->makeView(array(
'field' => $field,
'fieldConfig' => $fieldConf,
'moduleKey' => $moduleKey,
'tables' => $tables,
'connection' => $moduleConfig['data_source'],
)); if ($resp) return $resp;
} /**
* 保存某一字段的配置参数
* @return \Illuminate\Http\JsonResponse
*/
public function postFieldConfig()
{
$data = array(
'success' => true,
);
$moduleKey = Input::get('module_key');
$field = Input::get('field');
$moduleConfig = Config::get('crud/' . snake_case($moduleKey));
$fieldConf = &$moduleConfig['fields'][$field]; $postFormConf = Input::get('form');
$postListConf = Input::get('list');
$postRelationConf = Input::get('relation');
$fieldConf['form']['type'] = $postFormConf['type'];
if((
$postFormConf['type'] === 'select'
|| $postFormConf['type'] === 'radio'
|| $postFormConf['type'] === 'checkbox'
)
&& isset($postFormConf['options'])
&& $postFormConf['options']
){
$rawOptions = explode(',',$postFormConf['options']);
$options = array();
foreach($rawOptions as $option){
$options[$option] = $option;
}
$fieldConf['form']['options'] = $options;
}
$fieldConf['form']['placeholder'] = $postFormConf['placeholder'];
$fieldConf['form']['rule'] = $postFormConf['rule'];
$fieldConf['list']['sort'] = isset($postListConf['sort']);
$fieldConf['list']['search'] = $postListConf['search']; $fieldConf['relation'] = $postRelationConf; SiteHelpers::saveArrayToFile(app_path('config/crud/' . snake_case($moduleKey) . '.php'), $moduleConfig); return Response::json($data);
}
}

ModuleController

4.6 部署

DBuilder部署运行的操作系统可以是Windows或Linux,本文将基于LNMP(Linux+Nginx+MySQL+PHP)环境进行部署,详细部署环境要求:

  • PHP Version > 5.4
  • MCrypt PHP 必须安装
  • OpenSSL 必须安装
  • MySQL Version > 5.4
  • Nginx、Apache等服务器

首先,需要将DBuilder放置到Nginx的Default Server或者Vhost中,这里以Default Server为例。本文中DBuilder的根目录为

/home/wwwroot/dbuilder/

编辑nginx.conf文件,修改server节点:

server

{

listen 80 default_server;

#listen [::]:80 default_server ipv6only=on;

#server_name www.lnmp.org;

index index.html index.htm index.php;

root  /home/wwwroot/dbuilder;

#error_page   404   /404.html;

include enable-php.conf;

location / {

       try_files $uri $uri/ /index.php?$query_string;

   }

 

   location ~ [^/]\.php(/|$)

   {

          # comment try_files $uri =404; to enable pathinfo

          try_files $uri =404;

          fastcgi_pass  unix:/tmp/php-cgi.sock;

          #fastcgi_pass 127.0.0.1:9000;

          fastcgi_index index.php;

          include fastcgi.conf;

          #include pathinfo.conf;

   }

location /nginx_status

{

stub_status on;

access_log   off;

}

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$

{

expires      30d;

}

location ~ .*\.(js|css)?$

{

expires      12h;

}

location ~ /\.

{

deny all;

}

access_log  /home/wwwlogs/access.log  access;

}

修改DBuilder项目文件所属用户,保证nginx http进程对文件有读权限,本文部署环境中,nginx http进程为www用户进程;同时需要给部分DBuilder目录完全的写入权限,执行下列命令:

cd /home/wwwroot

chown –R www dbuilder

chgrp –R www dbuilder

cd dbuilder

chmod –R 777 app/storage

chmod -R 665 app/controllers/admin app/config/crud app/models/ app/views

建立数据库,在mysql中创建名为dbuilder的数据库,并source Dbuilder根目录下的dbuilder.sql,具体执行如下命令

# 首先进入msyql

mysql –uroot –pyour_root_password

# 进入mysql之后

create database dbuilder default char set utf8;

use dbuilder;

source /home/wwwroot/dbuilder/dbuilder.sql;

至此DBuilder部署完成,通过浏览器访问http://hostname/admin (hostname为主机域名或ip地址)即可以访问到DBuilder。

4.7 案例

设定:在不编写代码的基础上,以DBuilder生成一个简单可用的博客后台,博客后台有post表和category表,位于core数据源。

CREATE TABLE post

(

id INT(11) PRIMARY KEY NOT NULL,

category_id INT(11) NOT NULL,

title VARCHAR(64) NOT NULL,

short VARCHAR(256) NOT NULL,

content TEXT NOT NULL,

view_ct INT(11) DEFAULT '0' NOT NULL,

created_at TIMESTAMP DEFAULT 'CURRENT_TIMESTAMP' NOT NULL,

updated_at TIMESTAMP DEFAULT '0000-00-00 00:00:00' NOT NULL

) DEFAULT CHAR SET utf8;

CREATE TABLE category

(

id INT(11) PRIMARY KEY NOT NULL,

title VARCHAR(32) NOT NULL,

level INT(11),

weight INT(11) DEFAULT '0' NOT NULL COMMENT '排序字段',

parent_id INT(11),

post_ct INT(11) DEFAULT '0' NOT NULL,

) DEFAULT CHAR SET utf8;

4.7.1 新建GModule

准备好数据库表即可新建GModule,下面新建名为“Post”的GModule。进入GModule管理->新建界面,按图填写保存。

图4-1 新建GModule页面

编辑新建的Post GModule,可以看到在下部多出一个含有表格的tab。

图4-2 GModule Configuration字段配置页面

现在对于post表的所有字段都是默认配置,分别查看List和Form,可以看到List和Form都能正常读取数据库数据。

图4-3 GMoudle 列表页面

图4-4 GModule表单页面

上面两图呈现的List和Form并不具有可用性,因此需要对字段做配置。

4.7.2 GModule配置

首先修改字段的中文名、是否包含在form、是否包含在List等属性。

图4-5 GModule Configuration字段配置页面

保存之后,再次刷新Post列表和Form。对比图4-3、图4-4发现内容发生了变化

图4-6 GModule列表页面

图4-7 GModule表单页面

下面对每个字段做更详细的配置以得到更符合我们需求的页面,修改控件类型:short(摘要)字段为textarea(多行文本)类型,content(正文)字段为wysiwyg(富文本)类型,category_id字段为select(下拉列表)类型,updated_at(修改时间)为date(日期)类型。修改category_id(栏目外键)的关系为所属关系,并填写如下:

图4-8 GModule 字段详细配置表单

修改short(摘要)字段、title(标题)字段为不可排序与like模糊搜索,修改updated_at搜索方式为“>=”搜索

4.7.3 List&Form效果

刷新Post列表,可看到如下两个控件:date和select控件。

图4-9 GModule 列表搜索日期与下拉列表控件

输入搜索条件为修改日期:2016-03-03、栏目:C++、摘要:收到。结果按阅读次数排序。得到下面的列表结果。

图4-10 GModule 列表搜索与排序

点击其中一条记录进行编辑,测试Form功能。

图4-11 GModule编辑表单

修改之后点击保存也是正常可用的。

整个配置过程,只需几分钟,但却实现了上述较为复杂的功能。而如果换成开发人员手工编写类似功能模块,至少需要两三个小时的时间,相比之下,DBuilder极大的提高了开发效率。

第五章           总结与展望

本文基于WEB技术基本实现了一款可用的CRUD生成器,其内核实现比SximoBuilder更精简,在代码高度复用的前提下,提供更强的扩展性,并支持多数据库、前端验证、自定义表单控件等等。

由于时间原因,DBuilder尚未实现诸如用户管理、权限控制、操作日志记录、站点配置、多语言化等功能。另外,随着技术进步和网络普及,如何做到高并发、高性能以及支持数据集群的web系统是当前web项目开发需要着重考虑的问题。笔者将在DBuilder的后续改进中实现上述功能,并对高并发提供支持。同时,为了更好的推广和发展DBuilder,笔者已将DBuilder开源至Github:https://github.com/lvyahui8/dbuilder.git 。

最新文章

  1. iOS开发中多线程间关于锁的使用
  2. 实现Android的不同精度的定位(基于网络和GPS)
  3. 尽可能使用 const
  4. poj 1384 Piggy-Bank(全然背包)
  5. [USACO08JAN]手机网络Cell Phone Network
  6. centos下安装Vmware-tools时出现的问题
  7. 网络协议中HTTP,TCP,UDP,Socket,WebSocket的优缺点/区别
  8. redis+spring 整合
  9. Babel安装在本地并用webstrom由ES6转Es5
  10. 利用latex制作个人简历
  11. 菜鸟学SSH(十三)——Spring容器IOC解析及简单实现
  12. linux mount 挂接新硬盘
  13. python模块-json、pickle、shelve
  14. 下一代的DevOps服务:AIOps
  15. Redis学习(一):CentOS下redis安装和部署
  16. CCPC-Wannafly Winter Camp Day5 (Div2, onsite)
  17. log4j日志文件配置
  18. Ubuntu编绎 Objective C程序
  19. March 22 2017 Week 12 Wednesday
  20. ubuntu系统下的docker

热门文章

  1. Xcode中的Info.plist字段列表详解
  2. JavaScript(20)jQuery HTML 加入和删除元素
  3. 解决安装包在win7,win8系统下安装后运行没有管理员权限
  4. dos常用文件操作命令
  5. java_IO读写模版
  6. Using the EventManager
  7. linux 查看文件命令总结
  8. 检测 NSObject 对象持有的强指针
  9. 在aws ec2上使用root用户登录
  10. Android进阶笔记04:Android进程间通讯(IPC)之Messenger