控制器方法钩子(hook)
基本原理
hook
顾名思义,就是一个钩子,一个可以挂载到任何一个方法上面的钩子。它的方便之处在于不更改原方法,也可以让他执行。
案例
下面通过案例来一步步介绍如何制作这个钩子,钩子不能自动执行,必须通过后台的插件安装,进行注册。
实现一个带后台配置的插件,填写的配置可以输出到网站前台上。
- 后台 - 插件列表 - 下载 【极致插件DEMO】 这个插件,无需安装
- 在
app/admin/exts
复制test
文件夹,并重命名为newtest
,这个就是我们要制作的新插件 - 在
config.php
里面进行修改:
return [
'name'=>'新测试插件',//插件名,必须与插件文件夹名字相同
'desc'=>'这是一个极致插件开发的案例展示',//插件介绍
'author'=>'留恋风2581047041@qq.com',//作者介绍,这里可以把自己的联系方式带上去,方便用户沟通
'version'=>'1.0',//插件版本,默认1.0为最低版本
'update_time'=>'2022-02-24',//插件更新时间,格式:Y-m-d
'module'=>'Home',//插件应用的模块,Home表示前台模块,Admin表示后台模块.插件安装的时候会据此加载控制器到对应的目录中
];
- 打开
PluginsController.php
文件,下面很多可能用得上的函数,都做了注释,目前我们的需求只需要对,install
、uninstall
、setconfigdata
三个函数进行修改:
//执行SQL语句在此处处理,或者移动文件也可以在此处理
public function install(){
//下面是新增test表的SQL操作
/*
$table = 'test';
$sql = "CREATE TABLE IF NOT EXISTS `".DB_PREFIX.$table."` (
`id` int(11) unsigned NOT NULL auto_increment,
`tid` int(11) DEFAULT 0,
`orders` int(11) DEFAULT 0,
`comment_num` int(11) DEFAULT 0,
`htmlurl` varchar(100) DEFAULT NULL,
PRIMARY
KEY (`id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1";
$x = M()->runSql($sql);
*/
//转移一个文件到另一个文件下面
/**
* 方法1:将当前目录下的tpl/test.html 转移到 根目录static/test.html (需要确定转移的文件目录已存在)
* copy(APP_PATH.APP_HOME.'/exts/test/tpl/test.html',APP_PATH.'static/test.html')
* 方法2:将当前目录下的file文件夹下面所有文件(只是文件,不包含文件夹) 转移到 根目录static下面 (需要确定转移的文件目录已存在)
* $this->removeFile(APP_PATH.APP_HOME.'/exts/test/file',APP_PATH.'static');
* 方法3:将当前目录下的file文件夹下面所有文件(包含文件夹) 转移到 根目录static下面
* $this->recurse_copy(APP_PATH.APP_HOME.'/exts/test/file',APP_PATH.'static');
*/
//备份当前数据库
/**
* $this->JZ_backup();
*/
/**
这里进行注册 hook
**/
$ww = [];
$ww['module'] = 'home';
$ww['controller'] = 'Home';//HomeController前台主要入口控制器
$ww['action'] = 'jizhi';//主入口函数
$ww['hook_controller'] = 'NewTest';//我们需要创建的控制器 NewTestController
$ww['hook_action'] = 'index';//需要写的函数
$ww['all_action'] = 1;//这个标识,HomeController下面的所有方法,是否都执行我的这个函数,1是0否
$ww['isopen'] = 1;//开放这个钩子,必须写1
$ww['plugins_name'] = 'newtest';//我们这个插件文件夹名字
$ww['addtime'] = time();//当前时间,默认必须写
M('hook')->add($ww);
setCache('hook',null);//重置钩子,只要注册了钩子就必须重置才会执行
return true;
}
//卸载程序,对新增字段、表等进行删除SQL操作,或者其他操作
public function uninstall(){
//下面是删除test表的SQL操作
// $table = 'test';
// $sql = "DROP TABLE `".DB_PREFIX.$table."` ;";
// $x = M()->runSql($sql);
/**
注销hook
**/
M('hook')->delete(['plugins_name'=>'newtest']);
setCache('hook',null);//重置钩子,卸载后需要加载最新的缓存
return true;
}
//安装页面介绍,操作说明
public function desc(){
$this->display($this->tpl.'plugins-description.html');
}
//配置文件,插件相关账号密码等操作
public function setconf($plugins){
//将插件赋值到模板中
$this->plugins = $plugins;
$this->configs = json_decode($plugins['config'],1);//不能设置成 $this->config 因为已经设定了 private $config
$this->display($this->tpl.'plugins-body.html');
}
//获取插件内提交的数据处理
public function setconfigdata($data){
//$data包含了后台提交的所有内容
//我们在后台配置三个参数,分别是 title , description , litpic
$config['title'] = format_param($data['title'],1);
$config['description'] = format_param($data['description'],1);
$config['litpic'] = format_param($data['litpic'],1);
M('plugins')->update(['id'=>$data['id']],['config'=>json_encode($config,JSON_UNESCAPED_UNICODE)]);
JsonReturn(['code'=>0,'msg'=>'设置成功!']);
}
//批量转移文件
private function removeFile($from,$to){
//移动后台插件控制器
$sourcefile = $from;
$target = $to;
if(is_dir($sourcefile) && is_dir($target)){
if (false != ($handle = opendir ( $sourcefile ))) {
while ( false !== ($file = readdir ( $handle )) ) {
//去掉"“.”、“..”以及带“.xxx”后缀的文件
if ($file != "." && $file != ".." && !is_dir($sourcefile.'/'.$file) ) {
$fs = $sourcefile.'/'.$file;
$ft = $target.'/'.$file;
//备份源文件以防更新覆盖
copy($ft, $sourcefile.'/back/'.$file);
$r = $this->file2dir($fs,$ft);
if(!$r){
JsonReturn(array('code'=>1,'msg'=>'文件转移失败!sourcefile:'.$fs.' targetfile:'.$ft));
}
}
}
//关闭句柄
closedir ( $handle );
}
}
}
// 原目录,复制到的目录
function recurse_copy($src,$dst) {
$dir = opendir($src);
@mkdir($dst);
while(false !== ( $file = readdir($dir)) ) {
if (( $file != '.' ) && ( $file != '..' )) {
if ( is_dir($src . '/' . $file) ) {
$this->recurse_copy($src . '/' . $file,$dst . '/' . $file);
}
else {
copy($src . '/' . $file,$dst . '/' . $file);
}
}
}
closedir($dir);
}
//复制文件并转移
function file2dir($sourcefile, $filename){
if( !file_exists($sourcefile)){
return false;
}
//$filename = basename($sourcefile);
return copy($sourcefile, $filename);
}
//返回表字段
private function getTableFields($table){
if(defined('DB_TYPE') && DB_TYPE=='sqlite'){
$sql = "pragma table_info(".DB_PREFIX.$table.")";
$list = M()->findSql($sql);
$fields = [];
foreach($list as $v){
$fields[]=$v['name'];
}
}else{
$sql = 'SHOW COLUMNS FROM '.DB_PREFIX.$table;
$list = M()->findSql($sql);
$isgo = true;
$fields = [];
foreach($list as $v){
$fields[]=$v['Field'];
}
}
return $fields;
}
//返回数据库表
private function getTableData(){
if(defined('DB_TYPE') && DB_TYPE=='sqlite'){
$sql = "select name from sqlite_master where type='table' order by name";
}else{
$sql = "SHOW TABLES";
}
$tables = M()->findSql($sql);
$ttable = array();
foreach($tables as $value){
foreach($value as $vv){
$ttable[] = $vv;
}
}
return $ttable;
}
//备份数据库
private function JZ_backup(){
$pconfig = array(
'host' =>DB_HOST,
'port' =>DB_PORT,
'user' =>DB_USER,
'password' =>DB_PASS,
'database' =>DB_NAME
);
$this->config = array_merge($this->config, $pconfig);
$this->handler = new \PDO("mysql:host=".$this->config['host'].";port={$this->config['port']};dbname={$this->config['database']}", $this->config['user'], $this->config['password']);
$this->handler->query("set names utf8");
$this->backup();
}
/**
* 备份
* @param array $tables
* @return bool
*/
private function backup($tables = array())
{
//存储表定义语句的数组
$ddl = array();
//存储数据的数组
$data = array();
$this->setTables($tables);
if (!empty($this->tables))
{
foreach ($this->tables as $table)
{
$ddl[] = $this->getDDL($table);
$data[] = $this->getData($table);
}
//开始写入
$this->writeToFile($this->tables, $ddl, $data);
}
else
{
$this->error = '数据库中没有表!';
return false;
}
}
/**
* 设置要备份的表
* @param array $tables
*/
private function setTables($tables = array())
{
if (!empty($tables) && is_array($tables))
{
//备份指定表
$this->tables = $tables;
}
else
{
//备份全部表
$this->tables = $this->getTables();
}
}
/**
* 查询
* @param string $sql
* @return mixed
*/
private function query($sql = '')
{
$stmt = $this->handler->query($sql);
$stmt->setFetchMode(\PDO::FETCH_NUM);
$list = $stmt->fetchAll();
return $list;
}
/**
* 获取全部表
* @return array
*/
private function getTables()
{
$sql = 'SHOW TABLES';
$list = $this->query($sql);
$tables = array();
foreach ($list as $value)
{
$tables[] = $value[0];
}
return $tables;
}
/**
* 获取表定义语句
* @param string $table
* @return mixed
*/
private function getDDL($table = '')
{
$sql = "SHOW CREATE TABLE `{$table}`";
$ddl = $this->query($sql)[0][1] . ';';
return $ddl;
}
/**
* 获取表数据
* @param string $table
* @return mixed
*/
private function getData($table = '')
{
$sql = "SHOW COLUMNS FROM `{$table}`";
$list = $this->query($sql);
//字段
$columns = '';
//需要返回的SQL
$query = [];
foreach ($list as $value)
{
$columns .= "`{$value[0]}`,";
}
$columns = substr($columns, 0, -1);
$data = $this->query("SELECT * FROM `{$table}`");
foreach ($data as $value)
{
$dataSql = '';
foreach ($value as $v)
{
if($v==='' || $v===null){
$dataSql .= " NULL,";
}else{
$dataSql .= "'{$v}',";
}
}
$dataSql = substr($dataSql, 0, -1);
$query[]= "INSERT INTO `{$table}` ({$columns}) VALUES ({$dataSql});\r\n";
}
return $query;
}
/**
* 写入文件
* @param array $tables
* @param array $ddl
* @param array $data
*/
private function writeToFile($tables = array(), $ddl = array(), $data = array())
{
$public_str = "/*\r\nMySQL Database Backup Tools\r\n";
$public_str .= "Server:{$this->config['host']}:{$this->config['port']}\r\n";
$public_str .= "Database:{$this->config['database']}\r\n";
$public_str .= "Data:" . date('Y-m-d H:i:s', time()) . "\r\n*/\r\n";
$i = 0;
//echo '备份数据库-'.$this->config['database'].'<br />';
$countsql = 0;//记录sql数
$filenum = 0;//文件序号
$backfile = $this->config['target']==''? $this->config['database'].'_'.date('Y_m_d_H_i_s').'_'.rand(100000,999999): $this->config['target'].date('YmdHis');//文件名
$str = $public_str."SET FOREIGN_KEY_CHECKS=0;\r\n";
foreach ($tables as $table)
{
// echo '备份表:'.$table.'<br>';
$str .= "-- ----------------------------\r\n";
$str .= "-- Table structure for {$table}\r\n";
$str .= "-- ----------------------------\r\n";
$str .= "DROP TABLE IF EXISTS `{$table}`;\r\n";
$str .= $ddl[$i] . "\r\n";
$i++;
//echo '备份成功!<br/>';
}
$i = 0;
foreach($tables as $table){
//echo '备份表数据:'.$table.' <br>';
$str .= "-- ----------------------------\r\n";
$str .= "-- Records of {$table}\r\n";
$str .= "-- ----------------------------\r\n";
//$str .= $data[$i] . "\r\n";
foreach ($data[$i] as $v){
$str .= $v;
$countsql++;
if($countsql%($this->limit)==0){
$str = '<?php die();?>'.$str;
if($filenum==0){
$isok = file_put_contents('backup/'.$backfile.'.php', $str);
if(!$isok){
JsonReturn(['code'=>1,'msg'=>'[ backup/'.$backfile.'.php ] 写入文件失败!']);
}
$filenum++;
}else{
$isok = file_put_contents('backup/'.$backfile.'_v'.$filenum.'.php', $str);
if(!$isok){
JsonReturn(['code'=>1,'msg'=>'[ backup/'.$backfile.'_v'.$filenum.'.php ] 写入文件失败!']);
}
$filenum++;
}
$str = $public_str;
}
}
$i++;
}
if($str!='' && $str != $public_str){
$str = '<?php die();?>'.$str;
if($filenum==0){
$isok = file_put_contents('backup/'.$backfile.'.php', $str);
if(!$isok){
JsonReturn(['code'=>1,'msg'=>'[ backup/'.$backfile.'.php ] 写入文件失败!']);
}
}else{
$isok = file_put_contents('backup/'.$backfile.'_v'.$filenum.'.php', $str);
if(!$isok){
JsonReturn(['code'=>1,'msg'=>'[ backup/'.$backfile.'_v'.$filenum.'.php ] 写入文件失败!']);
}
}
}
}
- 将文件夹
newtest/controller/home
下面的TestController.php
进行重命名为NewTestController.php
,在上面install
里面写了NewTest
需要一一对应,并文件里面的TestController
改为NewTestController
,如下:
namespace app\home\plugins;
use app\home\c\CommonController;
class NewTestController extends CommonController
{
function index(){
echo '这是一个插件页面!';
//查询插件是否已经开启
$plugins = M('plugins')->find(['filepath'=>'newtest','isopen'=>1]);
if($plugins){
$config = json_decode($plugins['config']);//后台存储的json,所以要专为数组形式
$this->newtest = $config;//将插件赋值到模板中
}else{
Error('插件未开启!');
}
}
}
温馨提示:
controller/home
: 前台插件文件夹,安装后,自动将文件移动到app/home/plugins/
下面controller/admin
: 后台插件文件夹,安装后,自动将文件移动到app/admin/plugins/
下面
- 修改
newtest/tpl/plugins-description.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
{include="style"}
<style>
blockquote.layui-elem-quote {
font-size: 14px;
}
</style>
</head>
<body >
<div class="layui-rows" style=" margin: 10px;">
<h1 style="text-align:center">新的测试插件</h1>
<div class="layui-content" style="margin: 20px 40px;text-align: center;"><p style="font-size: 16px;">作者:留恋风2581047041@qq.com </span></p>
</div>
<div style=" margin: 25px; font-size: 20px;">
<fieldset class="layui-elem-field" style="border: none;padding: 0;border-top: 1px solid #eee;">
<legend>插件功能</legend>
<div class="layui-field-box">
<blockquote class="layui-elem-quote">这里写插件的一些功能说明...
</blockquote>
</div>
</fieldset>
</div>
</div>
</div>
<script>
layui.use('code', function(){ //加载code模块
layui.code({'encode':true}); //引用code方法
});
</script>
</body>
</html>
- 修改
newtest/tpl/plugins-body.html
:
<div class="layui-tab" style="margin:15px;">
<ul class="layui-tab-title">
<li class="layui-this">插件设置</li>
</ul>
<div class="layui-tab-content">
<div class="layui-tab-item layui-show">
<div class="layui-rows" style="margin: 10px;">
<form class="layui-form layui-form-pane" action="">
<blockquote class="layui-elem-quote">这个文件是后台插件的配置
</blockquote>
<input name="id" value="{$plugins['id']}" placeholder="这个插件ID必须带上" type="hidden">
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">插件的名称 </label>
<div class="layui-input-block">
<input type="text" name="title" value="{$configs['title']}" placeholder="如果写插件的名称" class="layui-input" />
</div>
</div>
<div class="layui-form-item layui-form-text">
<label class="layui-form-label">插件的简介 </label>
<div class="layui-input-block">
<textarea name="description" placeholder="请填写插件描述" class="layui-textarea">{$configs['description']}</textarea>
</div>
</div>
<div class="layui-form-item">
<label for="litpic" class="layui-form-label">
<span class="x-red">*</span>图片
</label>
<div class="layui-input-inline">
<input name="litpic" placeholder="上传图片" type="text" class="layui-input" id="litpic" value="{$configs['litpic']}" />
</div>
<div class="layui-input-inline">
<button class="layui-btn layui-btn-primary" id="litpic_upload" type="button" >选择图片</button>
</div>
<div class="layui-input-inline">
<img id="litpic_img" class="img-responsive img-thumbnail" style="max-width: 200px;" src="{$configs['litpic']}" onerror="javascipt:this.src='{__Tpl_style__}/style/images/nopic.jpg'; this.title='图片未找到';this.onerror=''">
<button type="button" onclick="deleteImage(this)" class="layui-btn layui-btn-sm layui-btn-radius layui-btn-danger " title="删除这张图片" >删除</button>
</div>
</div>
<div class="layui-form-item">
<div class="layui-input-block">
<button class="layui-btn" lay-submit lay-filter="formDemo">立即提交</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<script>
$(function () {
layui.use(['laydate','form','layer','upload'], function(){
$ = layui.jquery;
var form = layui.form
,layer = layui.layer,laydate = layui.laydate;
var upload = layui.upload;
upload.render({
elem: '#litpic_upload',
url: "{fun U('Common/uploads')}"
,data:{}
,done: function(res){
if(res.code==0){
$('#litpic_img').attr('src',res.url);
$('#litpic').val(res.url);
}else{
layer.alert(res.error, {icon: 5});
}
}
});
//监听提交
form.on('submit(formDemo)', function(data){
$.post("{fun U('setconf')}",data.field,function(res){
//console.log(res);return false;
var res = JSON.parse(res);
if(res.code==1){
layer.msg(res.msg);
}else{
layer.msg(res.msg, {icon: 6,time: 2000},function(){
window.location.reload();
});
}
})
return false;
});
});
})
</script>
- 至此,所有插件内容已经弄好,下面进行安装插件。
插件列表 -
本地
- 找到这个插件,点击安装
设置插件信息
插件列表,
开启
插件在前台模板
index.html
里面写:
插件名称:{$newtest['title']}
插件描述:{$newtest['description']}
图片:<img src="{$newtest['litpic']}" />
本章节的插件制作已经讲完,虽然有些繁琐,但是多尝试几遍,基本上是复制代码进行修改,还是比较简单易学的。