MVC框架实现CURD操作
思路
通过路由接收model
和view
参数,在控制器中分发请求,在编写过程中,发现原来通过一个control
文件就可以实现所有页面的分发。 只需要写好底层的container
服务容器与facade
门面类,让绑定类与生成实例不需要在客户端执行,在control
分发指令的时候向facade
传递model
与view
参数,就可以实现从路由解析——>Control分发请求——>最后再通过门面类返回渲染结果。
那么门面类需要做的工作就非常复杂,在编写的时候,为了解决服务容器实例化的问题头疼好久,如果每次都需要在客户端将服务容器实例化,再去绑定类和实例化方法,那么这样的代码一定是低效的,并且耦合性很高,如果需要修改一个类的生成方法,那么所有页面都需要重写,所以我就在想能不能让"生成服务容器、绑定类、生成实例"在服务端完成。
后来我用门面类继承了服务容器作为子类,在facade
门面类中将服务容器实例化,这样可以调用服务容器中的方法binds
和make
方法,再在facade
容器类中写两个静态方法来接收绑定与生成实例参数,分别传给服务容器的binds
和make
方法,最后通过路由参数动态绑定类和生成实例。
这样客户端代码就非常的简单了,不需要在客户端执行绑定和生成,只需要写一个通用controller
控制器,在控制器中向facade
传递model
与view
参数,通过静态类来动态的生成model
与view
类与实例,这样避免了代码耦合,维护起来更方便。
生成路由参数部分
namespace common; require __DIR__ . '/autoload.php'; define('_ROOT_','http://php.io/1206/'); //定义根目录 define('PAGES',['indexM','updateM','insertM','deleteM','selectM']); // 定义可以访问的页面 class Route{ protected static $request = null; protected static $mvc = []; protected static $params = []; public static function getMVC(){ static :: $request = explode( '/',$_SERVER['REQUEST_URI']); if( count(static :: $request) > 1){ // 判断数组长度是否大于2位,如果大于代表该路由含有MVC参数,并取出参数 // 截取MVC参数 static :: $request = array_slice(static :: $request, 2,3); list($control,$model,$view) = static :: $request; //给默认的index页面添加 M和V参数 // 由于是用变量名生成的类名,所以会出现找不到类的命名空间问题,所以需要手动添加路径 // 这里做一个简单的判断,如果访问的是默认index.php页面,没有传递Model和View参数,自动添加并生成index页面 if(empty($control)){ $control = 'index.php'; } if($control == 'index.php' && $model == '' && $view == ''){ $model .= 'common\models\\'.substr($control,0,strrpos($control,'.')).'M'; $view .= 'common\views\\'.substr($control,0,strrpos($control,'.')).'V'; }else if(in_array($model,PAGES)){ // 否则判断传递的Model参数是否包含在可访问页面中,如果没有,返回错误信息 $model = 'common\models\\'.$model; $view = 'common\views\\'.$view; }else{ exit('您访问的是非法路径,请返回<a href="'._ROOT_.'index.php">首页</a>'); } print_r($model,$view); print_r(in_array($model,PAGES)); echo '<hr>'; static :: $mvc = compact('control','model','view'); return static :: $mvc; } } public static function getParams(){ static :: $request = explode( '/',$_SERVER['REQUEST_URI']); if( count(static :: $request) > 6){ // 判断数组长度是否大于6位,如果大于代表该路由含有参数,并取出参数 // 截取参数 static :: $request = array_slice(static :: $request, 6); // 重打包到关联数组中 for($i = 0; $i<count(static::$request); $i+=2){ // 这里接收的参数,奇数是键名,偶数是值 if(isset(static::$request[$i+1])){ // $params的奇数值作为键名,$params的偶数值作为值,依次存储到$params参数 static :: $params[static::$request[$i]] = static :: $request[$i+1]; } } static :: $request = compact('model','view','control'); // 将MVC参数传入关联数组中 return static :: $params; } } }
在路由部分做了请求分析,如果用户访问的路径不是系统支持路径,或者添加其他参数,则提示访问路径错误,并返回首页。
由于是动态生成的model
和view
参数,对model
与view
参数做了处理,在前面添加了命名空间路径,避免找不到类的问题。
容器与门面类部分
namespace common; class Container{ // 实例数组 protected $instances=[]; // 绑定方法,接收类名和构造参数 public function binds($abstract,$concrete){ // 将类名存储到绑定中 $this->instances[$abstract] = $concrete; } // 将实例从容器中执行并返回,判断指向类是否已经绑定,如果绑定,生成该类实例,如果有构造参数,添加构造参数 public function make($abstract,...$params){ return call_user_func_array($this->instances[$abstract],$params); } } class Facade extends Container { protected static $container = null; protected static $data = []; // 继承容器类,先在门面类中将容器类实例化 public static function initialize(){ static :: $container = new Container(); } // 在门面类接收绑定参数,调用容器类中的binds方法,添加到容器类$instance属性中 public static function bind($abstract, $concrete){ static :: $container -> binds($abstract,$concrete); } public static function getData($model){ static :: $data = static:: $container -> make($model) -> getData(); } public static function fetchView($view){ static :: $container -> make($view) -> fetchView(static::$data); } public static function header($titleName){ static :: $container -> make('header',[$titleName]); } public static function footer(){ static :: $container -> make('footer',[]); } }
控制器部分
namespace common; class Controller{ protected $title = null; public function __construct($model,$view) { $this->title = substr($model,14); $this->title = substr($this->title,0,strrpos($this->title,'M')); switch($this->title){ case 'index': $this->title = '首页'; break; case 'update': $this->title = '大侠记录更新表'; break; case 'delete': $this->title = '大侠记录删除表'; break; case 'insert': $this->title = '大侠记录添加表'; break; case 'select': $this->title = '大侠记录查询表'; break; default: break; } Facade :: initialize(); //执行绑定,添加头部,尾部,Model和View对象 Facade :: bind('header',function($params=[]){return new header($params);}); Facade :: bind('footer',function($params=[]){return new footer($params);}); Facade :: bind($model,function() use($model){return new $model;}); Facade :: bind($view,function() use($view){return new $view;}); //渲染对象,将头部尾部,数据,实例化 Facade :: header($this->title); // 渲染头部,传递的参数是当前页面title标题 Facade :: getData($model); // 获取模型 Facade :: fetchView($view); // 渲染页面 Facade :: footer(); //渲染尾部 } }
在控制器中绑定了所有通用类,可以供所有页面调用,只需要将controller
实例化,再传输model
与view
参数即可。
最后是客户端部分
namespace _MVC1; use common\Route; use common\Controller; require __DIR__.'/common/autoload.php'; //获取路由 $route = Route::getMVC(); echo '<div style="width:500px;margin:0 auto;padding-top: 50px;"><pre>路由参数:'.print_r($route,true).'</pre></div>'; if($route == '非法路径'){ die('您访问的是'.$route); }else{ //渲染页面 $controller = new Controller($route['model'],$route['view']); }
首先调用路由分析静态类,再判断路由返回参数是否为 "非法路径",如果不是,则向controller
控制器传递路由中model
与view
参数,完成数据渲染。
至此,完成了一个控制器接收路由并分发model
与view
参数。
接下来是CURD部分:
首先是CURD封装类,该类支持简单的条件查询与排序和返回条目限制,并且为U和D操作添加了条件判断,如果用户输入条件为空,则不能执行该操作。
namespace common; use common\Db; require 'autoload.php'; class dbOperation{ public $pdo; public $sql; public function __construct(){ $this->pdo= Db::getInstance(); } private function substrPos($str,$operator){ //取指定符号之前字符方法 $str = substr($str,0,strrpos($str,$operator)); return $str; } private function substriPos($str,$operator){ //取指定符号之后字符方法 $str = substr($str,strripos($str,$operator)+1); return $str; } private function substrDel($str,$del){ //删除字符串末尾多余字符方法 $str=substr($str,0,strlen($str)-$del); return $str; } private function getOperator($con){ $operatorArr=['=','>','>=','<','<=','!=']; // 条件操作符集 //拆分WHERE为 字段 和 值,为预处理做准备 $whereParam=''; // WHERE的字段 $whereValue=''; // WHERE的条件 $operator=''; //返回操作符 $existOperator=0; // 判断是否存可执行条件符号 for($i=0;$i<count($operatorArr);$i++){ if(strstr($con,$operatorArr[$i])){ $whereParam=$this->substrPos($con,$operatorArr[$i]); // 获得拆分后的字段 $whereValue=$this->substriPos($con,$operatorArr[$i]); // 获得拆分后的值 $operator = $operatorArr[$i]; // 获取操作符 $existOperator=1; // 匹配到操作符,该条件成立 } } if(!$existOperator){ echo '暂时不支持该查询操作'; } $where=$whereParam.$operator.':'.$whereParam; //形参重写where条件 $con = [ $where,$whereParam,$whereValue ]; //将 形参条件,条件字段,条件值以数组形式返回 return $con; } public function operation($operation,$fields,$values,$table,$where='',$order='',$limit='') //查询语句 { $time = time(); //设置日期格式 if(empty($table) || empty($operation)){ echo '请输入要操作的表'; exit; } switch(strtoupper($operation)){ case 'UPDATE': if(is_array($fields) && is_array($values)) { if (count($fields) != count($values)) { echo '字段和值不匹配'; exit; }else if(empty($where)){ echo '该操作必须包含条件参数'; exit; }else{ list($where,$whereParam,$whereValue) = $this->getOperator($where); // 处理之后的WHERE语句,返回WHERE形参,WHERE字段,WHERE值 } } $updateParam = '`update`=:time'; //设置时间形参 $this->sql=$operation.' `'.$table.'` SET '; foreach($fields as $field_v){ $this->sql.='`'.$field_v.'`=:'.$field_v.', '; //设置形参,为预处理做准备 形参为:$field_v } $this->sql.=$updateParam; $this->sql.=' WHERE '.$where; ///设置查询条件 $stmt=$this->pdo->prepare($this->sql); //预处理SQL语句 foreach($values as $v_k => $v_v){ $stmt->bindValue(":{$fields[$v_k]}",$v_v); //循环绑定字段和值 } $stmt->bindValue(':time',$time); //绑定 TIME 更新时间 $stmt->bindValue(":{$whereParam}",$whereValue); //绑定 WHERE 条件 $num=$stmt->execute(); if($num > 0){ echo '<br>更新操作成功'; } $this->sql=null; //释放SQL语句 break; case 'DELETE': if(empty($where)){ echo '该操作必须包含条件参数'; exit; }else{ list($where,$whereParam,$whereValue) = $this->getOperator($where); // 处理之后的WHERE语句,返回WHERE形参,WHERE字段,WHERE值 } $this->sql=$operation.' FROM `'.$table.'` '.'WHERE '.$where; $stmt=$this->pdo->prepare($this->sql); $num=$stmt->execute([":{$whereParam}"=>$whereValue]); //绑定WHERE条件 if($num>0){ echo '<br>删除操作成功'; } $this->sql=null; //释放SQL语句 break; case 'INSERT': $insertField=''; // 处理添加字段 $insertParam=''; // 设置添加值形参 $dateField = '`date`'; //设置时间字段 $dateParam = ':time'; //设置时间形参 for($i=0;$i<count($fields);$i++){ $insertField.='`'.$fields[$i].'`,'; $insertParam.=':'.$fields[$i].','; } //循环添加字段形参 $insertField.=$dateField; $insertParam.=$dateParam; $this->sql=$operation.' INTO `'.$table.'` ('.$insertField.') VALUES ('.$insertParam.')'; $stmt=$this->pdo->prepare($this->sql); foreach($values as $v_k => $v_v){ $stmt->bindValue(":{$fields[$v_k]}",$v_v); } $stmt->bindValue(":time",$time); //绑定 日期 字段 $num = $stmt->execute(); if($num>0){ echo '<br>添加操作成功'; } $this->sql=null; //释放SQL语句 break; case 'SELECT' : $this->sql='SELECT '; if($fields[0] != '*'){ // 判断是否查询所有字段 foreach($fields as $field){ $this->sql.='`'.$field.'`,'; } $this->sql=$this->substrDel($this->sql,1); //删除末尾多余',' }//添加查询字段 else { $this->sql.='*'; } $this->sql.=' FROM `'.$table.'` '; if(!empty($where)) { list($where,$whereParam,$whereValue) = $this->getOperator($where); } empty($where) ?: $this->sql.='WHERE '.$where; empty($order) ?: $this->sql.=' ORDER BY '.$order; empty($limit) ?: $this->sql.=' LIMIT '.$limit; $stmt=$this->pdo->prepare($this->sql); // echo $this->sql.'<br>'; $stmt->bindValue($whereParam,$whereValue);//绑定WHERE条件 $stmt->execute(); // echo '<br><br>预处理语句生成<br><br>'; // $stmt->debugDumpParams(); //预处理语句调试 // echo '<br>'; // print_r($stmt->errorInfo()); // 错误信息捕捉 $stmt->setFetchMode(\PDO::FETCH_ASSOC); $result=$stmt->fetchAll(); echo '<table><thead><tr> <th>大侠姓名</th> <th>来自何处</th> <th>所习武功</th> <th>心法等级</th> </tr></thead>'; foreach($result as $row_v){ echo '<tr><td>' . $row_v['name'] . '</td> <td>' . $row_v['from'] . '</td> <td>' . $row_v['skill'] . '</td> <td>' . $row_v['level'] . '</td></tr>'; } echo '</table>'; echo '<span>总计'.$stmt->rowCount().'人</span>'; $this->sql=null; //释放SQL语句 break; default: break; } } public function __destruct(){ $this->pdo=null; echo '<p>连接已断开</p>'; } }
下面是封装类的操作语句示例:
$db->operation('UPDATE',array('name','from','skill','level'),array('乔峰','契丹','降龙十八掌','5'),'wuxia','id=14'); $db->operation('INSERT',array('name','from','skill','level'),array('虚竹','逍遥派','小无相功','8'),'wuxia'); $db->operation('SELECT',array('name','from','skill','level'),'','wuxia','level>7','level ASC','10'); $db->operation('DELETE','','','wuxia','id=13');
需要向该类传递要执行的操作,以及字段,表名,条件等。
下面是表单的action页面
namespace action; use common\dbOperation; require '../common/autoload.php'; $form = $_POST; $db = new dbOperation(); echo '<!doctype html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <link rel="stylesheet" href="'. \common\Root::$url.'css/style.css"> <title>返回结果</title> <style> p{ padding: 10px 20px; background: #e8e8e8; line-height: 1.5; width: 30%; margin: 20px auto; } </style> </head> <body><main>'; if($db->pdo){ echo '<p>连接成功</p>'; } //echo '<pre>'.print_r($form,true).'</pre>'; switch($form['op']){ case 'insert' : $db->operation('INSERT',array('name','from','skill','level'),array($form['name'],$form['from'],$form['skill'],$form['level']),'wuxia'); break; case 'update' : $db->operation('UPDATE',array('name','from','skill','level'),array($form['name'],$form['from'],$form['skill'],$form['level']),'wuxia','id='.$form['id']); break; case 'select' : $db->operation('SELECT',array('name','from','skill','level'),'','wuxia',$form['id'],$form['order'],$form['limit']); break; case 'delete' : $db->operation('DELETE','','','wuxia','id='.$form['id']); break; } echo '<a href="javascript:window.history.back(-1)">返回</a></main></body></html>';
在action页面中已经写好了4个语句的模版,通过用户传递过来的值来分析要执行的CURD操作,再把用户传递的值分别添加到每个语句中。
每个操作对应一个model
与view
文件。
整体文件结构。
下面是演示
基本路由分发
添加操作
更新操作
删除操作
查询操作
PHP文件上传
PHP文件上传需要注意以下几点:
扩展名设定
文件扩展名判断
文件大小设定
文件要重命名,避免重名冲突
设置接收上传文件的文件夹路径
PHP的超全局变量$_FILES
内置数组结构,本身是二维数组,可以参照以下结构自定义变量,拆分$_FILES
参数。
Array ( [file] => Array ( [name] => [type] => [tmp_name] => [error] => 4 [size] => 0 ) )
$files = $_FILES['file']; if(empty($files['name'])){ //如果$_FILES内的name键为空,则代表没有文件上传 echo '<script>alert("未上传文件");location.assign("index.html")</script>'; } $fileType = ['jpg','png','gif']; //设置可接受文件类型 $fileSize = $_POST['MAX_FILE_SIZE']; //隐藏域中设置的文件大小 $filePath = '/uploads/'; // 文件上传目录 $fileName = $files['name']; // 上传原始的文件名 $tempFile = $files['tmp_name']; //服务器上的临时文件名 $uploadError = $files['error']; // 返回错误代码,大于0代表有错误 if($uploadError>0){ switch ($uploadError) { case 1: case 2: die('上传文件不允许超过3M'); case 3: die('上传文件不完整'); case 4: die('没有文件被上传'); default: die('未知错误'); } } //判断文件扩展名是否正确 $findExtension = substr($fileName,strrpos($fileName,'.') + 1); //查找文件名中的'.'最后一次出现的位置,用substr函数切割字符串,起始位置为最后一次'.'出现的位置,结束位置为最后一位 if(!in_array($findExtension,$fileType)){ //检测获取到的文件名是否在支持的类型中 die ('不支持该类型文件上传'); // } //为了防止文件名重复,采用当前时间戳转换日期格式+md5加密随机数 echo '<hr>'; $fileName = date('YmdHis',time()) . md5(mt_rand(1,99)) . '.' . $findExtension; echo $fileName; //判断文件是否上传成功,首先判断是否是通过Post上传 if(is_uploaded_file($tempFile)){ if(move_uploaded_file($tempFile, __DIR__ . $filePath . $fileName)) {// 提示用户上成功,并返回上一个页面,再强行刷新当前页面 echo '<script>alert("上传成功");history.back();</script>'; }else { die('文件无法移动到指定目录,请检查目录权限'); } }else{ die('非法操作'); } exit();
总结
这几天一直在纠结MVC框架,几乎每天所有精力全部放在这上面了,在课堂上学习的东西一旦去实践的时候,脱离了课堂案例,感觉无从下手。
自己写代码也纠结,如果只是为了实现功能,那么这个作业也不难,但是在编写过程中,自己明明知道这样写是不通用的、低效的,就一直在就纠结怎么改,怎么让代码更优雅、更高效、更便于维护,这是一个程序员的基本思维。
写代码三句一报错,这几天每一天都在与Fatal Error, Warning
斗争!有的时候实在是找不到错误,不知道怎么改,就想实在写不出来的时候就想要不糊弄一下算了,就用低效的方法做好了,但是低效的代码实在是写不下去,可能这就是我纠结的点吧,知道自己可以做的更好,所以不能将就,怎么也要把错误解决。
在课堂上学习的东西,只有在实践的时候才会明白这些代码为什么这样写,才会明白老师为什么要这样教,也深深感觉到自己知识浅薄。这个MVC框架虽小,但是一个人要完成从"逻辑 ——命名空间——代码层次——文件层级管理",从数据层到表现层,全部都要自己去做,脑子里要对这些东西在构造层面去思考,再到具体实现,感觉到程序员真不是一个容易的工作,给用户呈现的只是一个简单的页面,背后却做了非常多的努力和工作。
这几天为了做这个框架耽误了好多进度,同学们都已经在做大作业了,希望我的这个小MVC框架能搭建到大作业上面,希望完成大作业过程中能顺利一些吧,给自己加油!