源碼位於github:https://github.com/lvyahui8/dbuilder.git 。文中圖片若是過小看不清楚,請右鍵點擊「在新標籤頁中打開」便可看到原圖php
有興趣還能夠加QQ羣交流:146103720 DBuilder交流羣
css
計算機軟件技術發展至今,數據庫已成爲最普遍使用的存儲格式化數據的媒介,數據庫程序開發技術也日趨完善,大型的ORM框架使得數據庫程序開發變得簡單,並已成爲操做關係型數據庫的主流方式。數據庫程序主要代碼爲CRUD(create, retrieve, update, delete)代碼,隨着ORM框架功能的完善,編寫CRUD代碼也衍生其固定的流程,對不一樣數據庫表進行操做的CRUD代碼也存在高度可重用性。當前編寫重複性的CRUD代碼成爲開發人員的常態,不只嚴重下降其積極性,並且損失其開發效率,因此迫切須要一種可以快速生成CRUD代碼的產品,以期減小這方面的工做,提升開發效率。html
目前國外已經誕生一些解決上述需求的、具備很高可用性的CRUD生成器產品:CrudKit,CRUD-Admin-Generator,Dadabik,GroceryCrud,SximoBuilder。這些產品各有其特色,但也有一共同點:都是基於PHP進行開發(這在必定程度上決定於PHP語法的靈活性及其解析性)。SximoBuilder是其中的典型表明,儘管SximoBuilder設計獨特、可用性高、流行度高,但也存在以下不足之處:前端
然而對於當今日益複雜的web程序,上述幾點是開發過程必須考慮的問題,所以,開發一款既具備SximoBuilder現有功能、又完善其不足之處的CRUD生成器產品,勢在必行。mysql
基於國內外CRUD生成器研究現狀,筆者開發一款名爲DBuilder(D爲DataAdministrator的簡寫)的CRUD 生成器。jquery
DBuilder借鑑SximoBuilder的模塊爲代碼單元、由模板生成代碼的思想,但擁有與SximoBuilder徹底不一樣的代碼生成器邏輯。它在實現SximoBuilder核心的代碼生成、通用CRUD兩種功能的基礎上,經過重寫代碼邏輯完善其不足之處:代碼冗餘度大、缺乏前端驗證。nginx
DBuilder面向的主要用戶人羣爲web後臺管理員以及開發人員,所以其系統分析過程,將更多的站在web後臺管理員及開發人員的角度考慮問題。git
項目須要實現以下幾點核心功能。github
1) 數據源管理web
用戶能夠在界面爲項目配置多個數據源。配置的數據源信息包括數據庫類型、地址、數據庫名、端口、用戶名、密碼等信息。支持對數據源的增刪改查,保證對數據源的增刪改查不輕易形成系統問題。
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等等。
按照需求提取可得實體有:用戶、用戶組、數據源、代碼模塊、菜單,關係有:權限、日誌。實體與關係的含義以下:
實體與關係的ER圖以下:
圖2-1 ER圖
DBuilder應該要作成一套高性能、高可用的CRUD生成器,爲此DBuilder設計中應該符合下面幾項原則:
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流程作詳細闡述。
Core CRUD 模塊實現核心CRUD操做,一切對GModule MVC中Controller的CRUD請求,最終轉交至Core CRUD 模塊進行處理。Core CRUD 模塊會開放一些預處理和後處理接口交由GModule實現,這些接口會在Model,Controller,View上都有體現。
Core CRUD 模塊主要包括以下文件
Core CRUD 模塊讀取GModule Configuration實現真正的CRUD操做。
GModule(Generated Module)不但實現了Core CRUD Module接口(MVC代碼),並且具備本身配置文件(CRUD Configuration)。每一GModule表示以一張數據庫表爲主表,具有CRUD功能的代碼文件合集(包括對應的MVC + Configuration代碼)。譬如,DBuilder生成的一個GModule, 主表爲core數據源user表,名字爲User,那麼User GModule應包含下面代碼文件:
代碼文件命名取決於GModule的名字,故爲保證生成的代碼文件不衝突,取GModule的名字(GModule Key,GModule Name)做爲GModule的惟一標識。每個GModule的信息都被保存在數據庫中。一次新建 GModule操做將會新建上述全部代碼文件,更新相關文件,並插入一條GModule記錄到數據庫。一次更新 GModule操做將只會更新Configuration文件。
GModule 由MVC代碼和CRUD Configuration代碼組成,下面分別進行闡述:
GModule並不表示具體某一個模塊,而是代指一類模塊,這種模塊能夠由DBuilder生成,或者由開發人員手工創建。它主要用來實現Core CRUD Module的接口,主要包括下述幾部分
1) Controller接口
假設GModule模塊的 Controller爲A,Core CRUD Module 的Controller爲B,則A應繼承自B。CRUD請求會先路由到A,而實際的處理者是B。A會實現B開放的下列接口。
2) Model 接口
GModule MVC代碼中的Model也繼承自BaseModel,實現 BaseModel類開放的一些接口能夠完成擴展。
formatXXXAttribute():該接口用來格式化某個字段。本產品基於Laravel,其已經具有相似的接口,就是getXXXXAttribute()。但這樣的接口的優先級比字段優先級高,這在特殊的狀況下爲開發帶來了不便,因此再設計一個相似的接口,該接口的優先級低於字段自己。
3) View 接口
視圖的擴展接口與前二者不一樣,主要體如今子視圖與視圖塊上,也就是在Core CURD模塊的視圖基礎上,擴展視圖組件。默認Core CRUD MVC視圖生成的是一個表格或者一個表單,佔滿頁面。而View接口將提供在該表格上下左右擴展頁面組件的能力。
4) Configuration
每個GModule對應一個Configuration文件,其中包含GModule對主表各個字段的配置參數,以及佈局參數。
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操做。
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實現預處理/後處理接口達到擴展性的目的。
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) |
|
用戶名 |
|
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 |
|
可導出 |
DBuilder須要支持多數據源,多種類型數據庫。數據源信息保存在d_database表中。考慮到數據庫操做是頻繁操做,若是將數據源信息保存在數據庫中,則每次數據庫操做將多一次數據源查詢操做,這樣作浪費性能。那麼DBuilder不該該把數據源信息保存在數據庫中,而應該保存在代碼文件中。數據源管理的信息包括數據源名稱(數據源的惟一標識,DBuilder默認的數據源名爲core)、數據庫類型、地址、端口、數據庫名、用戶名、密碼等等信息。由於數據源管理模塊並不對錶進行增刪改查操做,因此數據源管理模塊並非一個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路由。
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 表單在用戶輸入完成點擊保存以後,要分下面幾步:
Form 還須要開放對應的預處理和後處理接口。
3) 列表List(Table)
List是一個分頁Table,按照Module Configuration 中的字段配置顯示分頁數據。支持列表搜索,排序,勾選刪除,導出等功能;
4) 查看View
View 暫時以Form爲基礎,提供預處理後處理接口,但不容許編輯。
代碼按照前段資源、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的目錄。 |
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文件
1 <?php return array ( 2 'data_source' => 'core', 3 'table' => 'post', 4 'fields' => 5 array ( 6 'id' => 7 array ( 8 'label' => 'ID', 9 'form' => 10 array ( 11 'show' => true, 12 'hidden' => true, 13 'type' => 'text', 14 'rule' => 'required', 15 'ajax_validate' => false, 16 'placeholder' => '', 17 ), 18 'list' => 19 array ( 20 'show' => true, 21 'sort' => true, 22 'search' => '=', 23 'lookup' => false, 24 ), 25 'relation' => 26 array ( 27 'type' => '', 28 'table' => '', 29 'foreign_key' => '', 30 'show' => '', 31 'as' => '', 32 ), 33 ), 34 'title' => 35 array ( 36 'label' => '標題', 37 'form' => 38 array ( 39 'show' => true, 40 'hidden' => false, 41 'type' => 'text', 42 'rule' => 'required', 43 'ajax_validate' => false, 44 'placeholder' => '', 45 ), 46 'list' => 47 array ( 48 'show' => true, 49 'sort' => true, 50 'search' => '=', 51 'lookup' => false, 52 ), 53 'relation' => 54 array ( 55 'type' => '', 56 'table' => '', 57 'foreign_key' => '', 58 'show' => '', 59 'as' => '', 60 ), 61 ), 62 'short' => 63 array ( 64 'label' => '摘要', 65 'form' => 66 array ( 67 'show' => true, 68 'hidden' => false, 69 'type' => 'textarea', 70 'rule' => 'required', 71 'ajax_validate' => false, 72 'placeholder' => '', 73 ), 74 'list' => 75 array ( 76 'show' => true, 77 'sort' => true, 78 'search' => '=', 79 'lookup' => false, 80 ), 81 'relation' => 82 array ( 83 'type' => '', 84 'table' => 'category', 85 'foreign_key' => 'id', 86 'show' => 'id', 87 'as' => '', 88 ), 89 ), 90 'content' => 91 array ( 92 'label' => '正文', 93 'form' => 94 array ( 95 'show' => true, 96 'hidden' => false, 97 'type' => 'wysiwyg', 98 'rule' => 'required', 99 'ajax_validate' => false, 100 'placeholder' => '', 101 ), 102 'list' => 103 array ( 104 'show' => false, 105 'sort' => true, 106 'search' => '=', 107 'lookup' => false, 108 ), 109 'relation' => 110 array ( 111 'type' => '', 112 'table' => 'category', 113 'foreign_key' => 'id', 114 'show' => 'id', 115 'as' => '', 116 ), 117 ), 118 'view_ct' => 119 array ( 120 'label' => '查看次數', 121 'form' => 122 array ( 123 'show' => false, 124 'hidden' => false, 125 'type' => 'text', 126 'rule' => 'required', 127 'ajax_validate' => false, 128 'placeholder' => '', 129 ), 130 'list' => 131 array ( 132 'show' => true, 133 'sort' => true, 134 'search' => '=', 135 'lookup' => false, 136 ), 137 'relation' => 138 array ( 139 'type' => '', 140 'table' => '', 141 'foreign_key' => '', 142 'show' => '', 143 'as' => '', 144 ), 145 ), 146 'created_at' => 147 array ( 148 'label' => '建立時間', 149 'form' => 150 array ( 151 'show' => false, 152 'hidden' => false, 153 'type' => 'text', 154 'rule' => 'required', 155 'ajax_validate' => false, 156 'placeholder' => '', 157 ), 158 'list' => 159 array ( 160 'show' => false, 161 'sort' => true, 162 'search' => '=', 163 'lookup' => false, 164 ), 165 'relation' => 166 array ( 167 'type' => '', 168 'table' => '', 169 'foreign_key' => '', 170 'show' => '', 171 'as' => '', 172 ), 173 ), 174 'updated_at' => 175 array ( 176 'label' => '更新時間', 177 'form' => 178 array ( 179 'show' => false, 180 'hidden' => false, 181 'type' => 'text', 182 'rule' => 'required', 183 'ajax_validate' => false, 184 'placeholder' => '', 185 ), 186 'list' => 187 array ( 188 'show' => true, 189 'sort' => true, 190 'search' => '=', 191 'lookup' => false, 192 ), 193 'relation' => 194 array ( 195 'type' => '', 196 'table' => '', 197 'foreign_key' => '', 198 'show' => '', 199 'as' => '', 200 ), 201 ), 202 'category_id' => 203 array ( 204 'label' => '欄目', 205 'form' => 206 array ( 207 'show' => true, 208 'hidden' => false, 209 'type' => 'select', 210 'rule' => 'required', 211 'ajax_validate' => false, 212 'placeholder' => '', 213 ), 214 'list' => 215 array ( 216 'show' => true, 217 'sort' => true, 218 'search' => '=', 219 'lookup' => false, 220 ), 221 'relation' => 222 array ( 223 'type' => 'belongsTo', 224 'table' => 'category', 225 'foreign_key' => 'id', 226 'show' => 'title', 227 'as' => 'category_title', 228 ), 229 ), 230 ), 231 'list_options' => 232 array ( 233 'page' => 10, 234 'create' => true, 235 'update' => true, 236 'delete' => true, 237 ), 238 'form_options' => 239 array ( 240 'layout' => 241 array ( 242 'cols' => 12, 243 'label_cols' => 1, 244 'input_cols' => 11, 245 ), 246 ), 247 'relations' => 248 array ( 249 'id' => 250 array ( 251 'type' => '', 252 'table' => '', 253 'foreign_key' => '', 254 'show' => '', 255 'as' => '', 256 ), 257 'title' => 258 array ( 259 'type' => '', 260 'table' => '', 261 'foreign_key' => '', 262 'show' => '', 263 'as' => '', 264 ), 265 'short' => 266 array ( 267 'type' => '', 268 'table' => '', 269 'foreign_key' => '', 270 'show' => '', 271 'as' => '', 272 ), 273 'content' => 274 array ( 275 'type' => '', 276 'table' => '', 277 'foreign_key' => '', 278 'show' => '', 279 'as' => '', 280 ), 281 'view_ct' => 282 array ( 283 'type' => '', 284 'table' => '', 285 'foreign_key' => '', 286 'show' => '', 287 'as' => '', 288 ), 289 'created_at' => 290 array ( 291 'type' => '', 292 'table' => '', 293 'foreign_key' => '', 294 'show' => '', 295 'as' => '', 296 ), 297 'updated_at' => 298 array ( 299 'type' => '', 300 'table' => '', 301 'foreign_key' => '', 302 'show' => '', 303 'as' => '', 304 ), 305 'category_id' => 306 array ( 307 'type' => '', 308 'table' => '', 309 'foreign_key' => '', 310 'show' => '', 311 'as' => '', 312 ), 313 ), 314 );
數據源管理模塊完成基於網頁界面對app/config/datasource.php文件的配置。包含數據源列表頁,數據源新建與編輯頁。
實現數據源管理的核心控制器代碼放在DataSourceController.php文件中。
1 <?php 2 /** 3 * Created by PhpStorm. 4 * User: lvyahui 5 * Date: 2016/5/12 6 * Time: 15:35 7 */ 8 9 namespace admin; 10 11 use BaseModel; 12 use Illuminate\Support\Facades\Config; 13 use Illuminate\Support\Facades\Redirect; 14 use SiteHelpers; 15 use Illuminate\Support\Facades\Response; 16 use Illuminate\Support\Facades\Input; 17 18 use PDOException; 19 use PDO; 20 class DataSourceController extends AdminController 21 { 22 /** 23 * 呈現數據源列表 24 */ 25 public function getList() 26 { 27 $datasources = SiteHelpers::loadDataSources(); 28 $this->makeView(array( 29 'datasources' => $datasources, 30 )); 31 } 32 33 /** 34 * 異步加載某數據源的全部數據表 35 * @return \Illuminate\Http\JsonResponse 36 */ 37 public function getTables() 38 { 39 $dataSourceName = Input::get("data_source"); 40 $dataSources = SiteHelpers::loadDataSources(); 41 42 $dataSource = $dataSources[$dataSourceName]; 43 $tables = BaseModel::getTableList($dataSource['database'], $dataSourceName); 44 return Response::json(array( 45 'success' => true, 46 'data' => array( 47 'tables' => $tables, 48 'selected' => Input::get('table'), 49 ), 50 )); 51 } 52 53 /** 54 * 呈現數據源編輯或者新建FORM 55 * @param null $slug 56 */ 57 public function getEdit($slug = null) 58 { 59 $dataSource = null; 60 if ($slug) { 61 // 更新 62 $dataSource = $slug === 'core' ? Config::get('database.connections.core') 63 : Config::get('datasource.' . $slug); 64 $dataSource['name'] = $slug; 65 } else { 66 // 新建 67 $dataSource = array( 68 'name' => '', 69 'driver' => 'mysql', 70 'host' => 'localhost', 71 'port' => 3306, 72 'database' => '', 73 'username' => 'root', 74 'password' => '', 75 'charset' => 'utf8', 76 'collation' => 'utf8_unicode_ci' 77 ); 78 } 79 80 $this->makeView(array( 81 'dataSource' => $dataSource 82 )); 83 } 84 85 /** 86 * 測試數據源鏈接是否可靠 87 * @return \Illuminate\Http\JsonResponse 88 */ 89 public function postTest(){ 90 $success = true; 91 try{ 92 $dsn = Input::get('driver').':'.Input::get('host').':'.Input::get('port').';dbname='.Input::get('database'); 93 $dbh = new \PDO($dsn,Input::get('username'),Input::get('password')); 94 // $connection = new Connection($dbh,Input::get('database')); 95 // $key = md5(date("Y-m-d H:i:s")); 96 // DB::addConnection($key,$connection); 97 // if(!DB::connection($key)->getDatabaseName()){ 98 // $success = false; 99 // } 100 $dbh = null; 101 }catch(PDOException $e){ 102 $success = false; 103 } 104 return Response::json(array( 105 'success' => $success, 106 )); 107 } 108 109 /** 110 * 保存編輯好的數據源信息 111 * @param null $primaryKeyValue 112 * @return mixed 113 */ 114 public function postEdit($primaryKeyValue = null) 115 { 116 $dataSources = SiteHelpers::loadDataSources(); 117 $name = Input::get('name'); 118 $dataSources[$name] = Input::all(); 119 SiteHelpers::saveDataSources($dataSources); 120 121 return Redirect::action(get_class($this).'@getList'); 122 } 123 124 125 public function getTableFields(){ 126 $connection = Input::get('connection'); 127 $table = Input::get('table'); 128 129 $rawFields = BaseModel::getTableColumns($table,$connection); 130 131 $fields = array(); 132 $pri = null; 133 foreach($rawFields as $field){ 134 if($field->Key === 'PRI'){ 135 $pri = $field->Field; 136 } 137 $fields [] = $field->Field; 138 } 139 140 return Response::json(array( 141 'success' => true, 142 'data' => array( 143 'fields' => $fields, 144 'pri' => $pri, 145 ), 146 )); 147 } 148 }
CoreCRUD模塊涉及的代碼文件極其做用以下說明。
代碼文件以下
1 <?php 2 3 namespace admin; 4 5 use Illuminate\Support\Facades\Request; 6 use Illuminate\Support\Facades\Response; 7 use Illuminate\Support\Facades\Redirect; 8 use Illuminate\Support\Facades\Input; 9 use Illuminate\Support\Facades\Config; 10 use Illuminate\Support\Facades\View; 11 use Illuminate\Support\Facades\Cache; 12 use Illuminate\Support\Facades\URL; 13 use SiteHelpers; 14 use Module; 15 16 class AdminController extends \BaseController 17 { 18 protected $layout = 'layouts.admin.main'; 19 20 /** 21 * AdminController constructor. 22 */ 23 public function __construct() 24 { 25 parent::__construct(); 26 View::share('stdName',$this->getStdName()); 27 View::share('reducName',SiteHelpers::reducCase($this->getStdName())); 28 View::share('routeParams',$this->getRouteParams()); 29 if($this->model){ 30 $this->assignModel($this->model); 31 } 32 View::share('config',$this->savedConfig); 33 if(!Cache::has('modules')){ 34 Cache::forever('modules',Module::all()); 35 } 36 } 37 38 public function getList(){ 39 $models = $this->paginateModels(); 40 $view = $this->getRouteParam('c').'._list'; 41 if(!View::exists($view)){ 42 $view = 'admin.core.list'; 43 } 44 if(Request::ajax() || Input::has('isAjax')){ 45 return Response::json(array( 46 'success' => true, 47 'data' => array( 48 'models' => $models->toArray() 49 ) 50 )); 51 }else{ 52 $this->makeView(array( 53 'models' => $models, 54 $this->getStdName().'s' => $models, 55 ),$view); 56 } 57 } 58 59 public function getEdit($id = null) 60 { 61 if($id){ 62 $this->model = $this->model->find($id); 63 } 64 $data = array( 65 $this->model->getKeyName() => $id, 66 'model' => $this->model, 67 $this->modelName=>$this->model, 68 ); 69 $this->beforeEdit($data); 70 71 $view = $this->getRouteParam('c').'._form'; 72 if(!View::exists($view)){ 73 $view = 'admin.core.form'; 74 } 75 $this->makeView($data,$view); 76 } 77 78 protected function config(){ 79 $config = 'crud/'.$this->getStdName(); 80 if(!file_exists(app_path('config/').$config.'.php')){ 81 $config = 'crud/admin'; 82 } 83 return Config::get($config); 84 } 85 86 protected function assignModel($model) 87 { 88 $this->model = $model; 89 90 $config = $this->config(); 91 $defaultConfig = Config::get('crud/admin'); 92 $relations = array(); 93 /* 將默認參數傳遞給module config */ 94 foreach($config['fields'] as $field => &$fieldConfig){ 95 // if(isset($fieldConfig['value'])){ 96 // $this->model->$field = $fieldConfig['value']; 97 // } 98 $fieldConfig['form'] = array_merge( 99 $defaultConfig['fields']['field_name']['form'], 100 isset($fieldConfig['form']) ? $fieldConfig['form'] : array() 101 ); 102 $fieldConfig['list'] = array_merge( 103 $defaultConfig['fields']['field_name']['list'], 104 isset($fieldConfig['list']) ? $fieldConfig['list'] : array() 105 ); 106 if(isset($fieldConfig['relation']) && 107 isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] !== '' ){ 108 $relations[$field] = $fieldConfig['relation']; 109 } 110 } 111 112 $config['list_options'] = array_merge( 113 $defaultConfig['list_options'], 114 isset($config['list_options']) ? $config['list_options'] : array() 115 ); 116 117 $config['form_options'] = array_merge( 118 $defaultConfig['form_options'], 119 isset($config['form_options']) ? $config['form_options'] : array() 120 ); 121 122 /* 將字段的relation匯聚出來,是爲了後面的代碼方便,同時減小循環 */ 123 $config['relations'] = $relations; 124 125 $this->savedConfig = $config; 126 } 127 128 protected function paginateModels() 129 { 130 $models = array(); 131 if($this->model){ 132 $query = $this->model->newQuery(); 133 $this->handleListQuery($query); 134 $selects = array($this->model->getTable().'.*'); 135 136 foreach($this->savedConfig['relations'] as $field=>&$params){ 137 $query->join($params['table'],$params['table'].'.'.$params['foreign_key'],'=',$this->model->getTable().'.'.$field); 138 if(!isset($params['as'])){ 139 $params['as'] = $params['table'].'_'.$params['show']; 140 } 141 $selects[] = $params['table'].'.'.$params['show'] . ' as '.$params['as']; 142 } 143 $query->select($selects); 144 $orderBy = Input::get('list_order_by'); 145 if( $orderBy){ 146 $query->orderBy($this->model->getTable().'.'.$orderBy,Input::get('list_sort_asc') ? 'asc' : 'desc'); 147 }else{ 148 $query->orderBy($this->model->getTable().'.'.$this->model->getKeyName(),'desc'); 149 } 150 $page = Input::has('_page') ? Input::get('_page') : 10; 151 $models = $query->paginate($page); 152 } 153 return $models; 154 } 155 156 157 public function postEdit($primaryKeyValue = null){ 158 159 $primaryKeyName = $this->model->getKeyName(); 160 if($primaryKeyValue == null){ 161 $primaryKeyValue = Input::get($primaryKeyName); 162 } 163 164 $fields = $this->savedConfig['fields']; 165 $datas = array(); 166 foreach($fields as $field => $fieldConfig){ 167 if(Input::has($field)){ 168 $datas[$field] = Input::get($field); 169 } 170 } 171 172 if($primaryKeyValue){ 173 $this->model->where($primaryKeyName,$primaryKeyValue)->update($datas); 174 }else{ 175 $this->model->fill($datas); 176 $this->model->save(); 177 } 178 $this->afterSave($this->model); 179 $resp = Redirect::action(get_class($this).'@getList')->withMessage('save success!'); 180 181 return $resp; 182 } 183 184 public function getIndex() 185 { 186 $this->makeView(null,'admin.index'); 187 } 188 189 public function postDelete(){ 190 $ids = explode(',',Input::get('ids')); 191 $data = array(); 192 $success = true; 193 $data['ids'] = $ids; 194 $ids = array_filter($ids,function($id){ 195 return $id; 196 }); 197 $this->model->whereIn($this->model->getKeyName(),$ids)->delete(); 198 $data['redirect_url'] = URL::to(action(get_class($this).'@getList')); 199 return Response::json(array( 200 'success' => $success, 201 'data' => $data, 202 )); 203 } 204 205 public function getDelete($id){ 206 $this->beforeDelete($id); 207 $this->model->where($this->model->getKeyName(),$id)->delete(); 208 return Redirect::action(get_class($this).'@getList'); 209 } 210 211 public function missingMethod($parameters = array()) 212 { 213 // 214 $this->makeView(null,'site.404'); 215 } 216 217 protected function handleListQuery(&$query) 218 { 219 $searchFields = array_intersect_key($this->savedConfig['fields'],Input::all()); 220 foreach($searchFields as $field=> $fieldConfig){ 221 if(isset($fieldConfig['list']['search'])){ 222 $value = Input::get($field); 223 $operator = $fieldConfig['list']['search']; 224 if($value !== ''){ 225 if($operator){ 226 if($operator === 'like'){ 227 $value = '%'.$value.'%'; 228 } 229 $query = $query->where($this->model->getTable().'.'.$field,$operator,$value); 230 }else{ 231 $query = $query->where($this->model->getTable().'.'.$field,$value); 232 } 233 } 234 } 235 } 236 237 } 238 239 protected function beforeDelete($id) 240 { 241 242 } 243 244 protected function beforeEdit(&$data) 245 { 246 247 } 248 249 protected function afterSave($model) 250 { 251 252 } 253 254 public function getHelp(){ 255 $this->makeView(null,'admin.help'); 256 } 257 258 }
1 <?php 2 3 /** 4 * Created by PhpStorm. 5 * User: Administrator 6 * Date: 2015/10/10 0010 7 * Time: 17:58 8 */ 9 class BaseModel extends Eloquent 10 { 11 protected $table = ''; 12 protected $guarded = array('id'); 13 public $timestamps = false; 14 public static function getTranslates($translate){ 15 $rows = DB::table($translate['table'])->select(array($translate['foreign_key'],$translate['show']))->get(); 16 return $rows; 17 } 18 19 20 static function getTableList( $db ,$connection = null) 21 { 22 $t = array(); 23 $dbname = 'Tables_in_'.$db ; 24 $tables = $connection ? DB::connection($connection)->select("SHOW TABLES FROM {$db}") : DB::select("SHOW TABLES FROM {$db}"); 25 foreach($tables as $table) 26 { 27 $t[$table->$dbname] = $table->$dbname; 28 } 29 return $t; 30 } 31 32 static function getTableColumns( $table,$connection = false) 33 { 34 // $columns = array(); 35 $sql = "SHOW COLUMNS FROM $table"; 36 $rawColumns = $connection ? DB::connection($connection)->select($sql) 37 : DB::select($sql); 38 // foreach($rawColumns as $column) 39 // $columns[$column->Field] = $column->Field; 40 return $rawColumns; 41 } 42 43 function getColoumnInfo( $result ) 44 { 45 $pdo = DB::getPdo(); 46 $res = $pdo->query($result); 47 $i =0; $coll=array(); 48 while ($i < $res->columnCount()) 49 { 50 $info = $res->getColumnMeta($i); 51 $coll[] = $info; 52 $i++; 53 } 54 return $coll; 55 56 } 57 58 function builColumnInfo( $statement ) 59 { 60 $driver = Config::get('database.default'); 61 $database = Config::get('database.connections'); 62 $db = $database[$driver]['database']; 63 $dbuser = $database[$driver]['username']; 64 $dbpass = $database[$driver]['password']; 65 $dbhost = $database[$driver]['host']; 66 67 $data = array(); 68 $mysqli = new mysqli($dbhost,$dbuser,$dbpass,$db); 69 if ($result = $mysqli->query($statement)) { 70 71 /* Get field information for all columns */ 72 while ($finfo = $result->fetch_field()) { 73 $data[] = (object) array( 74 'Field' => $finfo->name, 75 'Table' => $finfo->table, 76 'Type' => $finfo->type 77 ); 78 } 79 $result->close(); 80 } 81 82 $mysqli->close(); 83 return $data; 84 85 } 86 87 static function findPrimarykey( $table, $db = null) 88 { 89 $query = "SHOW KEYS FROM `{$table}` WHERE Key_name = 'PRIMARY'"; 90 $primaryKey = ''; 91 $keys = $db ? DB::connection($db)->select($query) : DB::select($query); 92 93 foreach($keys as $key) 94 { 95 $primaryKey = $key->Column_name; 96 } 97 98 return $primaryKey; 99 } 100 }
1 <?php 2 $formOption = $config['form_options']; 3 $layout = $formOption['layout']; 4 $labelCols = $layout['label_cols']; 5 $inputCols = $layout['input_cols']; 6 $labelCss = "col-sm-$labelCols"; 7 $inputCss = "col-sm-$inputCols"; 8 // 插件是否加載 9 $loadUE = false; 10 $loadSBox = false; 11 $loadDatePicker = false; 12 ?> 13 <div class="panel panel-primary"> 14 <div class="panel-heading"> 15 <h3 class="panel-title">@if($model->id) 編輯<code>#{{$model->id}}</code>@else 新建 @endif</h3> 16 </div> 17 <div class="panel-body"> 18 <div class="row"> 19 <div class="col-sm-{{$layout['cols']}} col-sm-offset-{{(12-$layout['cols'])/2}}"> 20 <form class="form-horizontal validate" action="{{URL::to('admin/'.$stdName.'/edit')}}" method="post"> 21 <input type="hidden" name="{{$model->getKeyName()}}" value="{{$model->getKey()}}"> 22 <?php foreach($config['fields'] as $field => $settings):?> 23 <?php 24 if ($field === $model->getKeyName() || !$settings['form']['show']) continue; 25 $type = $settings['form']['type']; 26 $rule = $settings['form']['rule']; 27 ?> 28 <?php if($settings['form']['type'] === 'hidden'):?> 29 <input type="hidden" name="{{$field}}" value="{{$model->$field}}"> 30 <?php continue;?> 31 <?php endif; ?> 32 <div class="form-group"> 33 <label for="{{$field}}" 34 class="{{$labelCss}} control-label">{{isset($settings['label']) ? $settings['label'] : strtoupper($field)}}</label> 35 <div class="{{$inputCss}}"> 36 <?php if($type === 'textarea'):?> 37 <textarea name="{{$field}}" id="{{$field}}" rows="10" 38 {{SiteHelpers::inputValidate($rule)}} 39 class="form-control">{{$model->$field}}</textarea> 40 <?php elseif($type === 'select'):?> 41 <?php $loadSBox = true;?> 42 <select name="{{$field}}" id="{{$field}}" class="selectboxit" {{SiteHelpers::inputValidate($rule)}}> 43 @if (isset($settings['form']['options'])) 44 @if(is_array($settings['form']['options'])) 45 @foreach($settings['form']['options'] as $value => $text) 46 <option value="{{$value}}" 47 @if($value == $model->$field) selected @endif>{{$text}}</option> 48 @endforeach 49 @elseif(is_string($settings['form']['options'])) 50 @foreach($$settings['form']['options'] as $value => $text) 51 <option value="{{$value}}">{{$value}}</option> 52 @endforeach 53 @endif 54 @elseif(isset($settings['relation']['type']) && $settings['relation']['type'] ) 55 <?php 56 $fieldTranslate = $config['relations'][$field]; 57 $options = BaseModel::getTranslates($fieldTranslate); 58 foreach($options as $option): 59 ?> 60 <option value="<?=$option->$fieldTranslate['foreign_key']?>" 61 @if($option->$fieldTranslate['foreign_key'] == $model->$field) selected @endif 62 ><?=$option->$fieldTranslate['show']?> 63 </option> 64 <?php endforeach;?> 65 @endif 66 </select> 67 <?php elseif($type === 'wysiwyg'):?> 68 <?php 69 $loadUE = true; 70 ?> 71 <script type="text/plain" name="{{$field}}" id="wysiwyg-edit" 72 style="width:100%;height:240px;">{{$model->$field}}</script> 73 <?php elseif ($type === 'radio' || $type === 'checkbox'): ?> 74 75 @if(isset($settings['form']['options'])) 76 @foreach($settings['form']['options'] as $option => $text) 77 <div class="{{$type}} {{$type}}-replace"> 78 <input type="{{$type}}" value="{{$option}}" name="{{$field}}" 79 id="{{$field}}" @if($model->field === $option) checked @endif> 80 <label>{{$text}}</label> 81 </div> 82 @endforeach 83 @endif 84 <?php elseif($type === 'date'):?> 85 <?php $loadDatePicker = true;?> 86 <div class="input-group"> 87 <input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" {{SiteHelpers::inputValidate($rule)}}> 88 <div class="input-group-addon"> 89 <a href="#"><i class="entypo-calendar"></i></a> 90 </div> 91 </div> 92 <?php elseif($type === 'password'):?> 93 <input type="password" class="form-control" name="{{$field}}" id="{{$field}}" 94 value="{{$model->$field}}" 95 {{SiteHelpers::inputValidate($rule)}} 96 > 97 <?php elseif($type === 'file'):?> 98 <input type="file" 99 class="form-control file2 inline btn btn-primary" 100 data-label="<i class='glyphicon glyphicon-file'></i> 選擇文件" > 101 <?php else:?> 102 <input type="text" class="form-control" name="{{$field}}" id="{{$field}}" 103 {{SiteHelpers::inputMask($rule)}} 104 {{SiteHelpers::inputValidate($rule)}} 105 value="{{$model->$field}}"> 106 <?php endif;?> 107 </div> 108 </div> 109 <?php endforeach;?> 110 <div class="form-group"> 111 <div class="{{$inputCss}} col-sm-offset-{{$labelCols}}"> 112 <button type="submit" class="btn btn-primary">保存</button> 113 </div> 114 </div> 115 </form> 116 </div> 117 </div> 118 </div> 119 </div> 120 @yield('form.bottom','') 121 @section('styles') 122 @if($loadSBox) 123 {{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}} 124 @endif 125 @append 126 @section('scripts') 127 <?php if($loadUE):?> 128 {{HTML::script('plugins/ue-utf8-php/ueditor.config.js')}} 129 {{HTML::script('plugins/ue-utf8-php/ueditor.all.min.js')}} 130 {{HTML::script('plugins/ue-utf8-php/lang/zh-cn/zh-cn.js')}} 131 <?php endif;?> 132 @if($loadSBox) 133 {{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}} 134 @endif 135 @if($loadDatePicker) 136 {{HTML::script('assets/js/bootstrap-datepicker.js')}} 137 @endif 138 {{HTML::script('assets/js/jquery.inputmask.bundle.min.js')}} 139 {{HTML::script('assets/js/jquery.validate.min.js')}} 140 @append 141 142 @section('footScript') 143 <script> 144 var ue = null, 145 ueId = 'wysiwyg-edit'; 146 if (document.getElementById(ueId)) { 147 ue = UE.getEditor(ueId); 148 } 149 </script> 150 @append
1 @section('headStyle') 2 <style> 3 .sort.sort-active { 4 color: #000; 5 font-weight: bold; 6 } 7 </style> 8 @append 9 <?php 10 $list_options = $config['list_options']; 11 $loadSBox = false; 12 $loadDatePicker = false; 13 ?> 14 <div class="panel panel-default"> 15 <div class="panel-heading"> 16 <h3 class="panel-title"><?=isset($navMap[$stdName]['text']) ? $navMap[$stdName]['text'] : strtoupper($stdName)?> 17 列表</h3> 18 </div> 19 <div class="panel-body"> 20 <div class="row"> 21 <div class="col-sm-12"> 22 <div class="btn-group btn-group-sm" role="group"> 23 @if($list_options['create']) 24 <a href="{{URL::to('admin/'.$stdName.'/edit')}}" class="btn btn-primary">新建</a> 25 @endif 26 <a class="btn btn-danger delete-selected">刪除</a> 27 <a class="btn btn-default">導出</a> 28 </div> 29 </div> 30 </div> 31 <br> 32 <form class="list-form" action="" method="get"> 33 <input type="hidden" name="list_sort_asc" value="{{Input::get('list_sort_asc') !== null ? Input::get('list_sort_asc') : 1}}"> 34 <input type="hidden" name="list_order_by" value=""> 35 <table class="table table-bordered responsive table-hover table-striped"> 36 <thead> 37 <tr> 38 <th> 39 <div class="checkbox checkbox-replace"> 40 <input type="checkbox" class="item-all"> 41 </div> 42 </th> 43 <?php foreach($config['fields'] as $field=>$settings):?> 44 <?php if($settings['list']['show']):?> 45 <th @if($settings['list']['sort']) 46 class="sort @if($field === Input::get('list_order_by')) sort-active @endif" 47 data-field="{{$field}}" @endif 48 ><?=is_array($settings) && isset($settings['label']) ? $settings['label'] : strtoupper($field)?> 49 <span class="pull-right"> 50 @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 51 </span> 52 </th> 53 <?php endif;?> 54 <?php endforeach;?> 55 <th>操做</th> 56 </tr> 57 </thead> 58 <tbody> 59 <tr> 60 <td></td> 61 @foreach($config['fields'] as $field=>$fieldConfig) 62 @if($fieldConfig['list']['show']) 63 @if(isset($fieldConfig['list']['search']) && $fieldConfig['list']['search'] !== false) 64 <td> 65 @if($fieldConfig['form']['type'] == 'select' || ($fieldConfig['form']['type'] === 'radio' || $fieldConfig['form']['type'] == 'checkbox')) 66 <?php $loadSBox = true;?> 67 @if(isset($fieldConfig['form']['options']) && $fieldConfig['form']['options']) 68 <select name="{{$field}}" id="{{$field}}" class="selectboxit"> 69 <option value="" class="default-value">請選擇</option> 70 @foreach($fieldConfig['form']['options'] as $option => $text) 71 <option value="{{$option}}" @if(Input::get($field) && Input::get($field) === $option) selected @endif>{{$text}}</option> 72 @endforeach 73 </select> 74 @elseif(isset($fieldConfig['relation']['type']) && $fieldConfig['relation']['type'] ) 75 {{View::make('components.relation_select',array( 76 'fieldConfig'=>$fieldConfig,'field' => $field, ))}} 77 @endif 78 @elseif($fieldConfig['form']['type'] === 'date') 79 <?php $loadDatePicker = true;?> 80 <input type="text" name="{{$field}}" id="{{$field}}" class="form-control datepicker" data-format="yyyy-MM-dd" value="{{Input::get($field)}}"> 81 @else 82 <input type="text" name="{{$field}}" id="{{$field}}" 83 value="{{Input::get($field)}}" class="form-control input-sm"> 84 @endif 85 </td> 86 @else 87 <td></td> 88 @endif 89 @endif 90 @endforeach 91 <td> 92 <div class="btn-group btn-group-sm" role="group"> 93 <button type="submit" class="btn btn-primary">搜索</button> 94 <button type="reset" onclick="resetForm(this)" class="btn btn-warning hidden">重置</button> 95 </div> 96 </td> 97 </tr> 98 <?php foreach($models as $model):?> 99 <tr> 100 <td width="18px"> 101 <div class="checkbox checkbox-replace"> 102 <input type="checkbox" name="d_delete_select" class="item" value="{{$model->id}}"> 103 </div> 104 </td> 105 <?php foreach($config['fields'] as $filed=>$settings):?> 106 <?php if($settings['list']['show']):?> 107 <?php 108 $value = $model->$filed; 109 /* 字段在列表中須要翻譯 */ 110 if (array_key_exists($filed, $config['relations'])) { 111 $value = $model->$config['relations'][$filed]['as']; 112 } 113 ?> 114 <td>{{$value}}</td> 115 <?php endif;?> 116 <?php endforeach;?> 117 <td> 118 <div class="btn-group btn-group-sm" role="group"> 119 @if($list_options['update']) 120 <a href="{{URL::to('admin/'.$stdName.'/edit/'.$model->id)}}" 121 class="btn btn-primary">編輯</a> 122 @endif 123 @if($list_options['delete']) 124 <a href="{{URL::to('admin/'.$stdName.'/delete/'.$model->id)}}" 125 class="btn btn-danger">刪除</a> 126 @endif 127 @if(View::exists('admin.'.snake_case($stdName).'.list_item_links')) 128 @include('admin.'.snake_case($stdName).'.list_item_links',array('model'=>$model)) 129 @endif 130 </div> 131 </td> 132 </tr> 133 <?php endforeach;?> 134 </tbody> 135 </table> 136 </form> 137 <div class="pull-right"> 138 {{$models->appends(Input::all())->links()}} 139 </div> 140 </div> 141 </div> 142 143 @section('styles') 144 {{HTML::style('assets/js/datatables/responsive/css/datatables.responsive.css')}} 145 146 @if($loadSBox) 147 {{HTML::style('assets/js/selectboxit/jquery.selectBoxIt.css')}} 148 @endif 149 @append 150 151 @section('scripts') 152 {{HTML::script('assets/js/jquery.dataTables.min.js')}} 153 {{HTML::script('assets/js/datatables/jquery.dataTables.columnFilter.js')}} 154 @if($loadSBox) 155 {{HTML::script('assets/js/selectboxit/jquery.selectBoxIt.min.js')}} 156 @endif 157 @if($loadDatePicker) 158 {{HTML::script('assets/js/bootstrap-datepicker.js')}} 159 @endif 160 @append 161 162 @section('footScript') 163 <script> 164 $(document).ready(function(){ 165 $('th.sort').click(function(){ 166 var $th = $(this); 167 $('input[name="list_order_by"]').val($th.data('field')); 168 $('input[name="list_sort_asc"]').val($th.find('i').hasClass('fa-sort-asc') ? 0 : 1); 169 $('form.list-form').submit(); 170 }); 171 172 $('input.item-all').change(function(){ 173 var $this = $(this), 174 $items = $('input.item'); 175 if($this.is(':checked')){ 176 $items.prop('checked','checked'); 177 }else{ 178 $items.removeProp('checked'); 179 } 180 $items.trigger('change'); 181 }); 182 183 184 $('a.delete-selected').click(function(){ 185 var ids = [], 186 $items = $('input.item:checked'); 187 $items.each(function(i){ 188 ids.push($(this).val()); 189 }); 190 var idsStr = ids.join(','); 191 confirmModal({ 192 message : '確認刪除:'+idsStr, 193 onOk: function(){ 194 $.post('{{URL::to('admin/'.snake_case($stdName).'/delete')}}',{"ids":idsStr},function(resp){ 195 if(resp.success){ 196 window.location.href = resp.data.redirect_url; 197 } 198 },'json'); 199 } 200 }); 201 return false; 202 }); 203 }); 204 </script> 205 @append 206 207 @section('modals') 208 <div class="modal fade" id="confirm-modal" data-backdrop="static"> 209 <div class="modal-dialog"> 210 <div class="modal-content"> 211 <div class="modal-header"> 212 <h4 class="modal-title">操做確認</h4> 213 </div> 214 <div class="modal-body"> 215 216 </div> 217 <div class="modal-footer"> 218 <button type="button" class="btn btn-default cancel" data-dismiss="modal">取消</button> 219 <button type="button" class="btn btn-info ok">確認</button> 220 </div> 221 </div> 222 </div> 223 </div> 224 @stop
1 <?php 2 /** 3 * 說明: 4 * 1. 如下配置項,不設置即是默認 5 * Created by PhpStorm. 6 * User: lvyahui 7 * Date: 2016/5/2 8 * Time: 12:33 9 */ 10 11 return array( 12 13 /** 14 * 全部字段配置 15 */ 16 'fields' => array( 17 'field_name' => array( 18 /* 顯示在列表表格的表頭的內容,和form控件旁邊的內容*/ 19 'label' => '字段中文名', 20 /* 字段缺省值 */ 21 'value' => false, 22 /* 針對表單的設置 */ 23 'form' => array( 24 'show' => true, 25 'hidden' => false, 26 /* 27 * 字段對應表單的控件類型,默認text, 28 * 還支持經常使用的控件類型 29 * textarea 30 * radio 31 * checkbox 32 * number 33 * ipaddr 34 * wyswyg 35 * select 36 * date 37 * file 38 * 以及自定義類型 39 * */ 40 'type' => 'text', 41 /* 42 'type' => array( 43 'select' => array( 44 'options' => function(){ 45 return array(); 46 } 47 ), 48 ), 49 'type' => array( 50 'radio' => array(), 51 ), 52 */ 53 /* 提交表單後的驗證規則 */ 54 'rule' => 'required', 55 'ajax_validate' => false, 56 'placeholder' => 'xx', 57 58 ), 59 // 針對列表的設置 60 'list' => array( 61 /* 字段在列表是否顯示,默認爲顯示 */ 62 'show' => true, 63 /* 字段是否能夠排序,默認不能排序 */ 64 'sort' => true, 65 /* 是否可以按這個字段搜索 */ 66 'search' => true, 67 /* 字段進行翻譯,好比欄目Id字段,通常要轉成欄目名稱顯示 */ 68 'lookup' => false, 69 ), 70 71 ), 72 // more fields 73 ), 74 75 /** 76 * 全局form配置,優先級小於字段配置 77 */ 78 'form_options' => array( 79 'layout' => array( 80 'cols' => 12, 81 'label_cols' => 1, 82 'input_cols' => 11, 83 ), 84 ), 85 86 /** 87 * 全局list配置,優先級小於字段配置 88 */ 89 'list_options' => array( 90 'page' => 10, 91 'create' => true, 92 'update' => true, 93 'delete' => true, 94 ), 95 96 );
GModule是一類由DBuilder生成的模塊,它有一組模板定義在app/template目錄下:
前面設計中指出,GModule管理模塊自己是一個名爲「Module」,主表爲d_module,且手工創建的GModule,故其代碼組成也是符合GModule規範的,筆者編寫的代碼主要爲擴展代碼。GModule管理模塊對應了下述代碼文件:
下面貼上主要的代碼文件ModuleController.php
1 <?php 2 /** 3 * Created by PhpStorm. 4 * User: lvyahui 5 * Date: 2016/5/12 6 * Time: 15:28 7 */ 8 9 namespace admin; 10 11 define('MODULE_ROUTES', json_encode(include(app_path() . '/module_routes.php'))); 12 13 use Illuminate\Support\Facades\Redirect; 14 use SiteHelpers; 15 use BaseModel; 16 use Module; 17 use ConfigUtils; 18 use Illuminate\Support\Facades\Input; 19 use Illuminate\Support\Facades\Config; 20 use Illuminate\Support\Facades\Response; 21 22 class ModuleController extends AdminController 23 { 24 25 protected function beforeEdit(&$data) 26 { 27 $data ['dataSources'] = SiteHelpers::loadDataSources(); 28 if ($data['model']->id) { 29 $data ['moduleConf'] = ConfigUtils::get($data['model']->name); 30 } 31 } 32 33 protected function afterSave($module) 34 { 35 /* 生成代碼文件 */ 36 $codes = array( 37 'moduleName' => $module->name, 38 'moduleTitle' => $module->title, 39 'tablePrimaryKey' => BaseModel::findPrimarykey($module->db_table, $module->db_source), 40 'moduleNote' => $module->note, 41 'date' => date('Y-m-d'), 42 'dbSource' => $module->db_source, 43 'dbTable' => $module->db_table, 44 ); 45 $this->removeFiles($codes['moduleName']); 46 /* 生成默認module Configuration*/ 47 $moduleConfs = $this->buildConfiguration($module->db_table, $module->db_source); 48 SiteHelpers::saveArrayToFile(app_path('config/crud/') . snake_case($codes['moduleName']) . '.php', $moduleConfs); 49 50 $controller = file_get_contents(app_path('template') . '/controller.tpl'); 51 $model = file_get_contents(app_path('template') . '/model.tpl'); 52 $formView = file_get_contents(app_path('template') . '/_form.tpl'); 53 $listView = file_get_contents(app_path('template') . '/_list.tpl'); 54 $codes['timestamps'] = isset($moduleConfs['fields']['created_at']) && isset($moduleConfs['fields']['updated_at']) 55 ? 'true' : 'false'; 56 $buildController = SiteHelpers::blend($controller, $codes); 57 $buildModel = SiteHelpers::blend($model, $codes); 58 /* 生成 MVC 文件*/ 59 file_put_contents(app_path() . "/controllers/admin/{$codes['moduleName']}Controller.php", $buildController); 60 file_put_contents(app_path() . "/models/{$codes['moduleName']}.php", $buildModel); 61 $viewPath = app_path('/views/admin/') . snake_case($codes['moduleName']); 62 if (!file_exists($viewPath)) mkdir($viewPath); 63 file_put_contents($viewPath . "/_form.blade.php", $formView); 64 file_put_contents($viewPath . "/_list.blade.php", $listView); 65 66 67 /* 更新路由 */ 68 $moduleRoutes = json_decode(MODULE_ROUTES, true); //require(app_path().'/module_routes.php'); 69 if (is_array($moduleRoutes)) { 70 $moduleRoutes[SiteHelpers::reducCase($codes['moduleName'])] = 'admin\\' . "{$codes['moduleName']}Controller"; 71 SiteHelpers::saveArrayToFile(app_path() . '/module_routes.php', $moduleRoutes); 72 } 73 } 74 75 protected function beforeDelete($id) 76 { 77 $module = Module::find($id); 78 $moduleName = $module->name; 79 $this->removeFiles($moduleName); 80 } 81 82 /** 83 * 刪除GModule相關文件文件 84 * @param $moduleName 85 */ 86 public function removeFiles($moduleName) 87 { 88 $controller = app_path('admin/controllers') . "/{$moduleName}Controller.php"; 89 if (file_exists($controller)) { 90 unlink($controller); 91 } 92 $model = app_path('models') . "/{$moduleName}.php"; 93 if (file_exists($model)) { 94 unlink($model); 95 } 96 $moduleConf = app_path('config/crud/') . snake_case($moduleName) . '.php'; 97 if (file_exists($moduleConf)) { 98 unlink($moduleConf); 99 } 100 101 $viewPath = app_path('/views/admin/') . snake_case($moduleName); 102 $formFile = $viewPath . '/_form.blade.php'; 103 $listFile = $viewPath . '/_list.blade.php'; 104 if (file_exists($formFile)) unlink($formFile); 105 if (file_exists($listFile)) unlink($listFile); 106 } 107 108 private function buildConfiguration($table, $connection) 109 { 110 $rawColumns = BaseModel::getTableColumns($table, $connection); 111 $fields = ConfigUtils::build($rawColumns); 112 return array( 113 'data_source' => $connection, 114 'table' => $table, 115 'fields' => $fields, 116 ); 117 } 118 119 /** 120 * 獲取字段配置列表 121 * @return bool 122 */ 123 public function getFieldsConfig() 124 { 125 $filedsConfig = null; 126 if (Input::has('module_name')) { 127 $filedsConfig = ConfigUtils::get(Input::get('module_name'))['fields']; 128 } else { 129 $table = Input::get('table'); 130 $connection = Input::get('connection'); 131 132 $filedsConfig = $this->buildConfiguration($table, $connection)['fields']; 133 } 134 135 $resp = $this->makeView(array( 136 'fieldsConfig' => $filedsConfig, 137 )); 138 if ($resp) { 139 return $resp; 140 } 141 } 142 143 144 /** 145 * 保存字段列表配置 146 * @return mixed 147 */ 148 public function postSaveFieldsConf() 149 { 150 $resp = Redirect::action(get_class($this) . '@getEdit', Input::get('id')); 151 $postFields = Input::get('fields'); 152 $moduleName = Input::get('module_key'); 153 $confKey = SiteHelpers::reducCase($moduleName); 154 $savedConfig = ConfigUtils::get($confKey); 155 foreach ($savedConfig['fields'] as $fieldName => &$savefield) { 156 $postField = $postFields[$fieldName]; 157 $savefield['label'] = $postField['label']; 158 $savefield['form']['show'] = isset($postField['form']['show']); 159 $savefield['list']['show'] = isset($postField['list']['show']); 160 } 161 ConfigUtils::saveGModuleConf($confKey, $savedConfig); 162 return $resp; 163 } 164 165 /** 166 * 呈現某一字段的配置參數FORM 167 * @return bool 168 */ 169 public function getFieldConfig() 170 { 171 $moduleKey = Input::get('module_key'); 172 $field = Input::get('field'); 173 174 $moduleConfig = Config::get('crud/' . snake_case($moduleKey)); 175 $fieldConf = &$moduleConfig['fields'][$field]; 176 $dbSource = SiteHelpers::loadDataSources()[$moduleConfig['data_source']]; 177 $tables = BaseModel::getTableList($dbSource['database'],$moduleConfig['data_source']); 178 179 $resp = $this->makeView(array( 180 'field' => $field, 181 'fieldConfig' => $fieldConf, 182 'moduleKey' => $moduleKey, 183 'tables' => $tables, 184 'connection' => $moduleConfig['data_source'], 185 )); 186 187 if ($resp) return $resp; 188 } 189 190 /** 191 * 保存某一字段的配置參數 192 * @return \Illuminate\Http\JsonResponse 193 */ 194 public function postFieldConfig() 195 { 196 $data = array( 197 'success' => true, 198 ); 199 $moduleKey = Input::get('module_key'); 200 $field = Input::get('field'); 201 $moduleConfig = Config::get('crud/' . snake_case($moduleKey)); 202 $fieldConf = &$moduleConfig['fields'][$field]; 203 204 $postFormConf = Input::get('form'); 205 $postListConf = Input::get('list'); 206 $postRelationConf = Input::get('relation'); 207 $fieldConf['form']['type'] = $postFormConf['type']; 208 if(( 209 $postFormConf['type'] === 'select' 210 || $postFormConf['type'] === 'radio' 211 || $postFormConf['type'] === 'checkbox' 212 ) 213 && isset($postFormConf['options']) 214 && $postFormConf['options'] 215 ){ 216 $rawOptions = explode(',',$postFormConf['options']); 217 $options = array(); 218 foreach($rawOptions as $option){ 219 $options[$option] = $option; 220 } 221 $fieldConf['form']['options'] = $options; 222 } 223 $fieldConf['form']['placeholder'] = $postFormConf['placeholder']; 224 $fieldConf['form']['rule'] = $postFormConf['rule']; 225 $fieldConf['list']['sort'] = isset($postListConf['sort']); 226 $fieldConf['list']['search'] = $postListConf['search']; 227 228 $fieldConf['relation'] = $postRelationConf; 229 230 SiteHelpers::saveArrayToFile(app_path('config/crud/' . snake_case($moduleKey) . '.php'), $moduleConfig); 231 232 return Response::json($data); 233 } 234 }
DBuilder部署運行的操做系統能夠是Windows或Linux,本文將基於LNMP(Linux+Nginx+MySQL+PHP)環境進行部署,詳細部署環境要求:
首先,須要將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。
設定:在不編寫代碼的基礎上,以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; |
準備好數據庫表便可新建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並不具備可用性,所以須要對字段作配置。
首先修改字段的中文名、是否包含在form、是否包含在List等屬性。
圖4-5 GModule Configuration字段配置頁面
保存以後,再次刷新Post列表和Form。對比圖4-三、圖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搜索方式爲「>=」搜索
刷新Post列表,可看到以下兩個控件:date和select控件。
圖4-9 GModule 列表搜索日期與下拉列表控件
輸入搜索條件爲修改日期:2016-03-0三、欄目: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 。