【谨以此文,告诉大家,不会写代码也能做开发】
近日接到这么个需求,给Zabbix添加个交互。该交互首先需要给主机添加个自定义的ID,然后再在主机监测中,跳转到第三方系统,中间媒介就是自定义的ID。
需求便是如此,要做的事情就是:
1、在数据库中找到主机对应的表,添加字段,这里我们称之为deviceId;
2、在页面上增加写入的地方,然后让它保存到数据库中。
以这个需求为例,我们来看下Zabbix后台管理系统的大致架构。然后看下我又一次踩的坑。
首先要明确,Zabbix是一个设备管理的开源系统,且其后台管理系统是用php写的。
第一步,我们找到添加主机的页面,这里有创建主机的地方
观察其URL,访问的是hosts.php文件,由此可见,这部分代码在hosts.php文件中。
第二步:我们顺势打开hosts.php文件,一看,我勒个去,浩浩汤汤,1600多行代码!先不急着哭,我们发现URL里面还有个参数:form=create。我们把代码按分支收起来,找到(hasRequest('form'))的分支。这代码这么长,主要里面有很多if,我并非一个php的工程师,所以并不好说这种写法的优劣性。
第三步:在(hasRequest('form'))的分支内,一顿操作猛如虎啊,眼花缭乱,根本不知道做的啥跟啥,做完一大堆操作之后,最后来到一句话:
$hostView = new CView('configuration.host.edit', $data);
约莫猜着,这里返回了个视图。第一个参数目测是视图参数,在VSCode中一搜,发现这个似乎是个文件。于是我们打开了configuration.host.edit.php
【插播】
这里顺带说下,Zabbix里面的页面,基本没什么模板,也看不到多少html,清一色都是靠CView CDiv这些类给凑出来的。很有一种用C# Java写桌面程序的既视感。再不济也像写APP。厉害了我的PHP。
第四步:此时我们已经探索到了configuration.host.edit.php,大致观察了下,做了很多addRow的事情。直觉可以判断出,这里就是添加form表单的地方。这里我使出了多年的功力,“不会写代码也能开发”的绝技,我抄了一行。
->addRow(
(new CLabel(_('Host name'), 'host'))->setAsteriskMark(),
(new CTextBox('host', $data['host'], $data['readonly'], 128))
->setWidth(ZBX_TEXTAREA_STANDARD_WIDTH)
->setAriaRequired()
->setAttribute('autofocus', 'autofocus')
)
然后刷新页面(PHP就是这个好处,可以直接刷新),成功了,页面多出了一行元素,多了个输入框textbox。观察了下,里面的$data['host']就是要获取值用的。
第五步:
找到数据库,然后给对应的表增加个字段。Zabbix用的是MySQL,然后主机就是hosts,于是很容易找到hosts表,然后根据已有数据做出判断就是它,用sql语句添加个字段。并非难事,不细说了。
第六步:
是不是这样就可以在textbox里面写入新字段的内容,然后就能顺利地把新字段的填到数据库了呢?事实表明,我太年轻了。不然的话,就没来由这篇文章对Zabbix的后台管理系统进行浅析了。
当我发现并没有写进去的时候,我只能一步一步地跟着这份冗长的php代码步入进去。没办法,我既不是PHP工程师,也没研究过Zabbix,只能傻瓜式去深入。
第七步:
从业经验告诉我,点击保存是会触发某些动作的。回到hosts.php,发现这个动作就是‘add’,于是又找到了这部分代码的分支
elseif (hasRequest('add') || hasRequest('update')) {
try {
DBstart();
$hostId = getRequest('hostid', 0);
……………………
// Host data.
$host = [
'host' => getRequest('host'),
'name' => getRequest('visiblename'),
'status' => getRequest('status', HOST_STATUS_NOT_MONITORED),
'description' => getRequest('description'),
'proxy_hostid' => getRequest('proxy_hostid', 0),
'ipmi_authtype' => getRequest('ipmi_authtype'),
'ipmi_privilege' => getRequest('ipmi_privilege'),
'ipmi_username' => getRequest('ipmi_username'),
'ipmi_password' => getRequest('ipmi_password'),
'tls_connect' => getRequest('tls_connect', HOST_ENCRYPTION_NONE),
'tls_accept' => getRequest('tls_accept', HOST_ENCRYPTION_NONE),
'groups' => zbx_toObject($groups, 'groupid'),
'templates' => $templates,
'interfaces' => $interfaces,
'tags' => $tags,
'macros' => $macros,
'inventory_mode' => getRequest('inventory_mode'),
'inventory' => (getRequest('inventory_mode') == HOST_INVENTORY_DISABLED)
? []
: getRequest('host_inventory', [])
];
其中有一块很整齐的代码,就是把数据填入$host变量的。那我把这个填入不就完事了吗?满怀兴奋地做了测试,呵呵,我还是太年轻了,果然没写进去。我真是个LJ。
第八步:
来嘛,不服就干。上面填入$host变量之后,接下来干了什么事情呢?有句关键的代码
$hostIds = API::Host()->create($host);
喏,这不就是插入一条数据嘛。但这个,按照我写JS的习惯,$host里面的数据只要有了,应该都能写进去才对。为啥就写不进去的呢?只能想办法观察SQL语句了。接下来的问题又出现了,我去哪看这个sql语句呢?create函数在哪?
第九步:
我只知道,正常架构应该是要有类似于DAO层的东西,果然就看到了CHost的这个类,里面有Create方法。这个类的实例是有API这个工厂统一创建的。文件在API.php。
第十步:
Create方法里面的核心代码:
$hostid = DB::insert('hosts', [$host]);
这里就是调用DB的写入方法了。看来功夫不负有心人,快找到答案了。
直到来到这一层,$host变量的数据都是很正常的。我们要的自定义属性都是ok的。你说气人不气人,非得让我去到最后一层。
第11步:
既然如此,那就去到DB.php文件中的insert方法,把sql语句打印出来。
这个时候就呵呵了,sql语句中果然没有帮我把自定义的字段给插进去。
第12步:
观察下这部分代码:
foreach ($values as $key => $row) {
if ($getids) {
$resultIds[$key] = $id;
$row[$tableSchema['key']] = $id;
$id = bcadd($id, 1, 0);
}
self::checkValueTypes($table, $row);
$sql = 'INSERT INTO '.$table.' ('.implode(',', array_keys($row)).')'.
' VALUES ('.implode(',', array_values($row)).')';
if (!DBexecute($sql)) {
self::exception(self::DBEXECUTE_ERROR, _s('SQL statement execution has failed "%1$s".', $sql));
}
}
这里的操作就是一个sql语句拼接的过程,其中$row变量就是存储了字段信息。分别在不同的阶段观察了下$row的内容,发现了在checkValueTypes之前,$row还保留了我的自定义字段,checkValueTypes之后,就消失了!
答案已经揭晓,必然是checkValueTypes做了什么对不住我的事情。你个负心婆娘。
第13步:
前往checkValueTypes捉奸。
大致看下这个代码:
public static function checkValueTypes($table, &$values) {
global $DB;
$tableSchema = self::getSchema($table);
foreach ($values as $field => $value) {
if (!isset($tableSchema['fields'][$field])) {
unset($values[$field]);
continue;
}
if (isset($tableSchema['fields'][$field]['ref_table'])) {
if ($tableSchema['fields'][$field]['null']) {
$values[$field] = ($values[$field] == '0') ? NULL : $values[$field];
}
}
(太长懒得copy)
这里大概的意思就是判断字段是否有效。而且还做了个操作:
$tableSchema = self::getSchema($table);
大致可以判断是根据表名获取了数据结构。看来捉奸只来到了酒店前台,还没到房间。那就去getSchema这个房间看看。
第14步:
大致看下代码:
public static function getSchema($table = null) {
if (is_null(self::$schema)) {
self::$schema = include(dirname(__FILE__).'/../../'.self::SCHEMA_FILE);
}
if (is_null($table)) {
return self::$schema;
}
elseif (isset(self::$schema[$table])) {
return self::$schema[$table];
}
else {
self::exception(self::SCHEMA_ERROR, _s('Table "%1$s" does not exist.', $table));
}
}
这里就拉取了一个文件,文件名定义在self::SCHEMA_FILE里面。摸到这个文件的定义:
const SCHEMA_FILE = 'schema.inc.php';
然后打开这个文件,豁然开朗,原来所有表结构的定义都在这个文件里面,如果没定义在这个文件里面的字段,就会在checkValueTypes被过滤掉,从而写不进去!
于是,就在'schema.inc.php'加上我想要的字段,刷新,执行测试,再看数据库,终于都写进去了。
长吁一口气。
此次的探查,大致摸清了Zabbix的代码结构。以host为例,自顶向下大致如下分层:
hosts.php ----controller层
configuration.host.edit.php ----view层
CHost.php----service层,API是实现了工厂模式
DB.php----util层,db用于数据库交互。
schema.inc.php---配置层,用于配置信息。
其他不同的文件可以大致对应过去。
这就是今天我作为一个非PHP的程序员,用最原始的echo和print_r,活生生地抄出了个功能。手段简陋到我都不太好意思,十分唏嘘。最后说一句:PHP是这个世界上最好的语言。
----------------------后记-------------------------
后来同事在我这个基础上去修改,还是没成功,检查了下还少了个地方
【当你看到这,说明你还是很感兴趣,码字不易,点个赞点个关注再走呗,哈哈】