MVC PHP架构 博客论坛实现全过程

目录

1. MVC的历史

  MVC开始是存在于桌面程序中的,M是指业务模型,V是指用户界面,C则是控制器,使用MVC的目的是将M和V的实现代码分离,从而使同一个程序可以使用不同的表现形式。比如一批统计数据可以分别用柱状图、饼图来表示。C存在的目的则是确保M和V的同步,一旦M改变,V应该同步更新。

  模型-视图-控制器(MVC)是Xerox PARC在二十世纪八十年代为编程语言Smalltalk-80发明的一种软件设计模式,已被广泛使用。后来被推荐为Oracle旗下Sun公司Java EE平台的设计模式,并且受到越来越多的使用ColdFusion和PHP的开发者的欢迎。模型-视图-控制器模式是一个有用的工具箱,它有很多好处,但也有一些缺点。

  • 框架模式:MVC、MTV、MVP、CBD、ORM等等;是一种代码结构组织的方法,跟功能实现没有太多关系
  • 框架:C++语言的QT、MFC、gtk,Java语言的SSH 、SSI,php语言的 smarty(MVC模式),python语言的django(MTV模式)等等
  • 设计模式:工厂模式、适配器模式、策略模式等等
  • 简而言之:框架是大智慧,用来对软件设计进行分工;设计模式是小技巧,对具体问题提出解决方案,以提高代码复用率,降低耦合度。
  • 框架通常是代码重用,而设计模式是设计重用。

1.1 优点与缺点

1.1.1 优点

  1. 耦合性低
    视图层和业务层分离,这样就允许更改视图层代码而不用重新编译模型和控制器代码,同样,一个应用的业务流程或者业务规则的改变只需要改动MVC的模型层即可。因为模型与控制器和视图相分离,所以很容易改变应用程序的数据层和业务规则。
  2. 重用性高
    MVC模式允许使用各种不同样式的视图来访问同一个服务器端的代码,因为多个视图能共享一个模型,它包括任何WEB(HTTP)浏览器或者无线浏览器(wap),比如,用户可以通过电脑也可通过手机来订购某样产品,虽然订购的方式不一样,但处理订购产品的方式是一样的。由于模型返回的数据没有进行格式化,所以同样的构件能被不同的界面使用。
  3. 部署快,生命周期成本低
    MVC使开发和维护用户接口的技术含量降低。使用MVC模式使开发时间得到相当大的缩减,它使程序员(Java开发人员)集中精力于业务逻辑,界面程序员(HTML和JSP开发人员)集中精力于表现形式上。
  4. 可维护性高
    分离视图层和业务逻辑层也使得WEB应用更易于维护和修改。

1.1.2 缺点

  1. 完全理解MVC比较复杂
    由于MVC模式提出的时间不长,加上同学们的实践经验不足,所以完全理解并掌握MVC不是一个很容易的过程。
  2. 调试困难
    因为模型和视图要严格的分离,这样也给调试应用程序带来了一定的困难,每个构件在使用之前都需要经过彻底的测试。
  3. 不适合小型,中等规模的应用程序
    在一个中小型的应用程序中,强制性的使用MVC进行开发,往往会花费大量时间,并且不能体现MVC的优势,同时会使开发变得繁琐。
  4. 增加系统结构和实现的复杂性 运行速度慢
    对于简单的界面,严格遵循MVC,使模型、视图与控制器分离,会增加结构的复杂性,并可能产生过多的更新操作,降低运行效率。
  5. 视图与控制器间的过于紧密的连接并且降低了视图对模型数据的访问
    视图与控制器是相互分离,但却是联系紧密的部件,视图没有控制器的存在,其应用是很有限的,反之亦然,这样就妨碍了他们的独立重用。
    依据模型操作接口的不同,视图可能需要多次调用才能获得足够的显示数据。对未变化数据的不必要的频繁访问,也将损害操作性能。

2. 个人博客论坛的MVC实现

2.1 前言

整个项目用到了Jquery,Php,Mvc架构。一开始是在虚拟机上,用phpStudy去做,最后放到了阿里云的服务器上,用宝塔配置环境,就在宝塔面板进行编程测试了。这个方法比较方便。

一开始做的时候是跟着知乎的 水巷先生 去了解web和夯实js,css,html,php的基础。但是到后也只是达到了一个入门的水平。

接着又在B站发现了传智播客周洋老师的一个课程2017年的,这技术几乎有点落伍了,但是完完整整的把这个项目写出来了。

初期编写的时候,很舒服,看着PHP代码一点一点的累积,整个web变得完整起来。初始老师介绍MVC架构,跟着老师手写了PHP架构。心情愉悦。

中期编写时,利用一个博客的前后台视图模板,MVC架构,堆积功能。一个功能说听起来不多,但是整个涉及到的增删改查写到吐血,一个小功能控制器需要几天才能写完。煎熬。

后期编写时,最大的困难就是无法对web视图进行控制,前面的一些编写已经让自己有了一些想法,不过老觉得页面不好看,自己却没有能力改,这个真是让人一言难尽。

历时一个多月,这个php项目写的差不多了,还有一些小的逻辑漏洞,以及博客升级为论坛已经有了一个底子,但是用户个人页面还没有做。也不打算做了,一些重复的活。

切实感到程序员的辛苦,真的时一坐一天,没有了基础的运动,感觉身体都不太好了。又经常遇到bug,没有人可以给你解决,一扣就是半天....

唉,编程苦也!

下面就是对整个编写过程的收获,网站的逻辑进行一次总结。算是一种检藏。

2.2 web代码结构 框架

  在web应用越来越复杂的当下,代码也越来越多,越来越复杂,那么我们如何高效的组织和管理代码呢?这是一个问题

2.2.1 web应用发展

  • 混编模式:脚本的业务逻辑,数据交互,用户视图放到一个文件当中 index.php.   这种方法后续维护的难度非常大,但是他的运行效率是最高的。
  • 显示逻辑分离:将 php 的逻辑代码和 html 显示的代码分家,使用时用include './templates/view.html' (包含)。
    模板文件: HTML主要负责展示的功能,其中可变的数据是用动态脚本PHP来填充!这样的一种混编文件我们一般就叫作“模板文件”!模板文件中的PHP代码通常只负责输出数据,而不负责处理数据!所以,在html中的php代码,使用的最多的就是echo,foreach、while!
    用户不应该请求负责展示的模板文件!需要我们对模板文件隐藏起来!
    典型的,就是通过apache的分布式配置文件来完成!
    • 此时,需要在主配置文件中允许分布式配置文件对主配置文件进行重写!
      .htaccess文件:Deny form all
  • MVC: 网站很多功能的核心就是 数据+操作 同时,很多功能的数据是各个独立数据的拼凑,在逻辑和显示的基础上,我们将逻辑中的有关数据操作的代码提取出来,在功能需要的时候调用即可。

2.2.2 Controller,Model,View

在整个的web里,Controller似乎就是功能的聚合,换句话说,实现功能就是实现对应的Controller。

Controller还起到粘合Model,View的作用。在Controller里接受数据,调用Model模型进行数据库连接,Model对表操作执行sql语句以关联数组的方式返回一个查询结果变量。在视图中需要的地方填入变量即可。

通常的做法是一张表对应一个模型类,模型类里存放了许多增删改查的函数。而模型类又继承自基础模型类。

基础模型类里有一个数据库连接的实例,这样就不需要每个模型类都去繁琐的连接数据库了。

如果在一个功能(控制器)中,需要使用某个表的多次操作,应该使用该表的一个模型就可以完成全部的任务!用单例工厂来实现 Factory.class.php

2.3 基础模型类 Model.class.php

获取数据库连接实例,后续模型只需要继承调用 $this->dao->fetchAll($sql);就可以进行表操作了。

2.3.1 Model.class.php

<?php
class Model{
	protected $dao;
	public function __construct(){
		$this->initDao();
	}
	private function initDao(){
		// 初始化数组 存放数据库连接的参数 数据库名 数据库密码等
		$config = $GLOBALS['conf']['db'];
		//获得数据库联接对象实例
		switch($GLOBALS['conf']['App']['dao']){
			case 'pdo' : $dao_class = 'PDODB';break;
			case 'mysql': $dao_class = 'MySQLDB';break;
		};
		$this->dao = $dao_class::getInstance($config);
	}
}

2.4 模型类的单例模式 Factory.class.php

如果在一个功能(控制器)中,需要使用某个表的多次操作,应该使用该表的一个模型就可以完成全部的任务!

根据参数 自动 new 一个对象,若对象存在则返回原来的对象.

php很多的参数大都是字符串类型,即文件名变成了一个变量了,需要什么文件,传变量就是了。

2.4.1 Factory.class.php

<?php
	/**
	 * 生成模型类的单例对象
	 * @param string $class_name
	 * @return object
	 */
class Factory{
	public static function M($class_name){
		static $model_list = array();
		// 之前$model_list['$class_name'] 加了单引号(会变成$class_name  所有的class的数组名字都一样,但是
		// new 的还是传进来的类) 在多次实例中就不能用了
		if(!isset($model_list[$class_name])){
			$model_list[$class_name] = new $class_name;
		}
		return $model_list[$class_name];
	}
}
?>

2.5 一个控制器类的实例 后台ArticleControler.class.php

<?php
/**
* 后台文章功能的控制器
* 首页文章列表管理的展示   indexAction()
* 添加文章                addAction()
* 处理添加文章            dealAddAction()
* 编辑修改文章            editAction()
* 处理编辑修改文章        dealEditAction()
* 删除文章               delAction()
* 批量删除文章           delAllAction()
* 回收站页面             recycleAction() 
* 回收站删除             realDelAction() 
* 回收站批量删除         realDelAllAction() 
* 回收站的批量删除和恢复  DelorRelAllAction() 升级可以处理批量的删除和恢复
* 文章推荐的动作         ifRecommendAction()
**/
class ArticleController extends PlatformController{
	/**
	* 文章展示
	**/
	public function indexAction(){
                // 实例化文章模型 用于后续的数据获取
		$article = Factory::M('ArticleModel');
		// 以下代码与分页有关
		$pageNum = isset($_GET['pageNum']) ? $_GET['pageNum'] : 1; //当前页数
		$rowsPerPage = $GLOBALS['conf']['Page']['rowsPerPage']; //每页几个文章
		// 例如 第2页 roperPage=5, ofset=5 limit 5,5 即展示 5+1---10
		$offset = ($pageNum-1)*$rowsPerPage;  //偏移量
		$artInfo = $article->getArt($offset,$rowsPerPage);
		$this->smarty->assign('artInfo',$artInfo);
		
		$maxNum = $GLOBALS['conf']['Page']['maxNum'];
		$url = "index.php?p=Back&c=Article&a=index&";
		$rowCount = $article->getRowCount(0); // 获取总记录数
		// 实例化分页类
		$page = new Page($rowsPerPage, $rowCount, $maxNum, $url);
		$strPage = $page->getStrPage();
		// 分配页码字符串
		$this->smarty->assign('strPage', $strPage);
		// 分页到此结束
		
		// 调用视图文件
		$this->smarty->display('art_index.html');
	}
	
	/**
	* 添加文章
	**/
	public function addAction(){
		
		//调用分类模型,上传文章时,便于选择分类 有一个缺点是没有  未分类
		$category = Factory::M('CategoryModel');
		$cateInfo = $category->getCategory();
		$this->smarty->assign('cateInfo',$cateInfo);
		$this->smarty->display('art_add.html');
		
	}
	/**
	* 处理文章的添加操作
	**/
	public function dealAddAction(){
		
		$art = array();
		
		// 表中有10个字段 art_id, addtime, is_del, thumb(缩略图), hits(点击次数) 没有填写
		$art['cate_id'] = $this->filterData($_POST['cate_id']);
		$art['title'] = $this->filterData($_POST['title']);
		
		$art['art_desc'] = $this->filterData($_POST['art_desc']);
		// 使用文本编辑器后, 文章里很多比标签 ,不能使用filterData 加个转义斜杠就行了
		$art['content'] = addslashes($_POST['content']);
		$art['author'] = $this->filterData($_POST['author']);
		
		// 数据判断
		if(empty($art['author']) || empty($art['title']) || empty($art['art_desc']) || empty($art['content'])){
			
			$this->jump('index.php?p=Back&c=Article&a=add',':(您有未填写字段');	
		}
		if(empty($art['cate_id']) ){
			$this->jump('index.php?p=Back&c=Article&a=add',':(请选择文章分类');	
		}
		// 判断是否有缩略图上传
		if($_FILES['thumb']['error'] != 4) {
			// 说明用户选择了上传文件,实例化上传类
			$upload = Factory::M('Upload');
			// 初始化相关参数
			$allow = array('image/jpeg', 'image/png', 'image/gif', 'image/jpg');
			$path = UPLOADS_DIR . 'thumb';
			// 调用uploadAction方法
			$result = $upload->uploadAction($_FILES['thumb'], $allow, $path);
			// 判断是否上传成功
			if($result) {
			    // 生成缩略图
			    $max_w = 175;
			    $max_h = 115;
			    $src_file = UPLOADS_DIR . 'thumb/'. $result;
			    $image = Factory::M('Image');
			    if($thumb = $image->makeThumb($max_w, $max_h,$src_file, $path )){
			        // 销毁源文件 只保留缩略图信息
			        unlink( $src_file);
			        $art['thumb'] = $thumb;
			    }else{
			        $this->jump('index.php?p=Back&c=Article&a=add',Image::$error);
			    }
			}else {
				// 记录错误信息并跳转
				$error = Upload::$error;
				$this->jump('index.php?p=Back&c=Article&a=add', $error);
			}
		}else {
			$art['thumb'] = 'default.jpg';
		}
		
		// 调用模型 插入数据
		
		$article = Factory::M('ArticleModel');
		
		$result = $article->insertArt($art);
		
		if($result){
			$this->jump('index.php?p=Back&c=Article&a=index');	
		}else{
			$this->jump('index.php?p=Back&c=Article&a=add',':(发生未知错误 文章发布失败');	
		}
	}
	/*
	* 编辑文章
	**/
	public function editAction(){
		
		//  获取文章id
		$art_id = $_GET['art_id'];
		// 获取文章详细信息
		$article = Factory::M('ArticleModel');
		$artInfoById = $article->getArtInfoById($art_id);
		
		// 分配变量
		$this->smarty->assign('artInfoById', $artInfoById);
		// 获取文章类别信息
		$category = Factory::M('CategoryModel');
		$cateInfo = $category->getCategory();
		// 分配变量
		$this->smarty->assign('cateInfo', $cateInfo);
		// 输出视图文件
		$this->smarty->display('art_edit.html');	
	}
	/**
	* 处理文章的编辑操作
	**/	
	public function dealEditAction(){
		
		$art = array();
		// 隐藏的art_id
		$art['art_id'] = $this->filterData($_POST['art_id']);
		// 表中有10个字段  addtime, is_del, thumb(缩略图), hits(点击次数) 没有填写
		$art['cate_id'] = $this->filterData($_POST['cate_id']);
		$art['title'] = $this->filterData($_POST['title']);
		
		$art['art_desc'] = $this->filterData($_POST['art_desc']);
		// 使用文本编辑器后, 文章里很多比标签 ,不能使用filterData 加个转义斜杠就行了
		$art['content'] = addslashes($_POST['content']);
		$art['author'] = $this->filterData($_POST['author']);
		
		// 数据判断
		if(empty($art['author']) || empty($art['title']) || empty($art['art_desc']) || empty($art['content'])){
			
			$this->jump("index.php?p=Back&c=Article&a=edit&art_id={$art['art_id']}",':(您有未填写字段');	
		}
		if(empty($art['cate_id']) ){
			$this->jump("index.php?p=Back&c=Article&a=edit&art_id={$art['art_id']}",':(请选择文章分类');	
		}
		// 判断是否有缩略图上传
		if($_FILES['thumb']['error'] != 4) {
			// 说明用户选择了上传文件,实例化上传类
			$upload = Factory::M('Upload');
			// 初始化相关参数
			$allow = array('image/jpeg', 'image/png', 'image/gif', 'image/jpg');
			$path = UPLOADS_DIR . 'thumb';
			// 调用uploadAction方法
			$result = $upload->uploadAction($_FILES['thumb'], $allow, $path);
			// 判断是否上传成功
			if($result) {
				if($_POST['thumb_bak'] !== 'degault.jpg'){
					//unlink() 函数删除 旧的图片文件。
					unlink(UPLOADS_DIR . 'thumb/'.$_POST['thumb_bak']);
				}
				$art['thumb'] = $result; // 将新名字记录到数组 因为插入和更新都需要名字,后面会用到
			}else {
				// 记录错误信息并跳转
				$error = Upload::$error;
				$this->jump("index.php?p=Back&c=Article&a=edit&art_id={$art['art_id']}", $error);
			}
		}else {
			$art['thumb'] = $_POST['thumb_bak']; // 没有新图上传 用以前的名字
		}
		
		// 调用模型 插入数据
		
		$article = Factory::M('ArticleModel');
		
		$result = $article->updateArtById($art);
		
		if($result){
			$this->jump("index.php?p=Back&c=Article&a=index");	
		}else{
			$this->jump("index.php?p=Back&c=Article&a=edit&art_id={$art['art_id']}",':(发生未知错误 文章发布失败');	
		}
	}
	/**
	* 文章的删除操作
	**/
	public function delAction(){
		//  获取文章id
		$art_id = $_GET['art_id'];
		// 删除文章
		$article = Factory::M('ArticleModel');
		$result = $article->delArtById($art_id);
		//结果判断跳转
		if($result){
			$this->jump('index.php?p=Back&c=Article&a=index');	
		}else{
			$this->jump('index.php?p=Back&c=Article&a=index',':(发生未知错误 文章删除失败');	
		}
	}
	/**
	* 文章的批量删除操作 最重要的就是接受$_POST['art_ids']数组,并处理成sql可以懂的字符串类型,
	**/
	public function delAllAction(){
		
		if(!isset($_POST['art_id'])) {
			// 说明没有没有选择文章
			$this->jump('index.php?p=Back&c=Article&a=index', ':( 请先选择彻底要删除的文章!');
		}
		// 接受传来的数组,并处理成sql可以懂的字符串类型
		$art_ids = implode(',',$_POST['art_id']);
		
		// 调用模型 逻辑删除数据
		$article = Factory::M('ArticleModel');
		$result = $article->delAllArt($art_ids);
		if($result){
			$this->jump("index.php?p=Back&c=Article&a=index");	
		}else{
			$this->jump("index.php?p=Back&c=Article&a=index",':(发生未知错误 文章批量删除失败');	
		}
		
	}
	
	/**
	* 回收站的批量删除和恢复 DelorRelAllAction()
	**/
	public function DelorRelAllAction(){
		
		// 先判断用户是否选择了文章
		if(!isset($_POST['art_id'])) {
			// 说明没有没有选择文章
			if(isset($_POST['is_del']) && $_POST['is_del'] == '批量删除'){
				$this->jump('index.php?p=Back&c=Article&a=recycle', ':( 请先选择要删除的文章!');
			}else{
				$this->jump('index.php?p=Back&c=Article&a=recycle', ':( 请先选择要删除的文章!');
			}
			
		}
		// 接受传来的数组,并处理成sql可以懂的字符串类型
		$art_ids = implode(',',$_POST['art_id']);
		
		// 调用模型 逻辑删除数据
		$article = Factory::M('ArticleModel');
		if(isset($_POST['is_del']) && $_POST['is_del'] == '批量删除')
		{
			$result = $article->delAllArt($art_ids);
			if($result){
				$this->jump("index.php?p=Back&c=Article&a=recycle");	
			}else{
				$this->jump("index.php?p=Back&c=Article&a=recycle",':(发生未知错误 文章批量删除失败!');
			}
		}elseif(isset($_POST['is_del']) && $_POST['is_del'] == '批量恢复'){
			$result = $article->recoverAllArt($art_ids);
			if($result){
				$this->jump("index.php?p=Back&c=Article&a=recycle");	
			}else{
				$this->jump("index.php?p=Back&c=Article&a=recycle",':(发生未知错误 文章批量恢复失败');	
			}
		}else{
			$this->jump("index.php?p=Back&c=Article&a=recycle",':(发生未知错误 文章批量操作失败');	
		}
		
		
	}

	/**
	* 回收站  recycleAction() 
	**/
	public function recycleAction(){
		
		/*$article = Factory::M('ArticleModel');
		
		$artInfo = $article->getDelArt();
		
		$this->smarty->assign('artInfo',$artInfo);
		// 调用视图文件
		$this->smarty->display('art_recycle.html');*/
		
		$article = Factory::M('ArticleModel');
		// 以下代码与分页有关
		$pageNum = isset($_GET['pageNum']) ? $_GET['pageNum'] : 1; //当前页数
		$rowsPerPage = $GLOBALS['conf']['Page']['rowsPerPage']; //每页几个文章
		// 例如 第2页 roperPage=5, ofset=5 limit 5,5 即展示 5+1---10
		$offset = ($pageNum-1)*$rowsPerPage;  //偏移量
		$artInfo = $article->getDelArt($offset,$rowsPerPage);
		$this->smarty->assign('artInfo',$artInfo);
		
		$maxNum = $GLOBALS['conf']['Page']['maxNum'];
		$url = "index.php?p=Back&c=Article&a=recycle&";
		$rowCount = $article->getRowCount(1); // 获取总记录数
		// 实例化分页类
		$page = new Page($rowsPerPage, $rowCount, $maxNum, $url);
		$strPage = $page->getStrPage();
		// 分配页码字符串
		$this->smarty->assign('strPage', $strPage);
		// 分页到此结束
		
		// 调用视图文件
		$this->smarty->display('art_recycle.html');
	}
        /**
        * 回收站文章单个还原
        **/
	public function recoverAction(){
		
		$art_id = $_GET['art_id'];
		// 还原文章
		$article = Factory::M('ArticleModel');
		$result = $article->recoverArtById($art_id);
		//结果判断跳转
		if($result){
			$this->jump('index.php?p=Back&c=Article&a=recycle');	
		}else{
			$this->jump('index.php?p=Back&c=Article&a=recycle',':(发生未知错误 文章还原失败');	
		}
		
	}
	/**
        * 回收站文章单个删除
        **/
	public function realDelAction(){
		
		$art_id = $_GET['art_id'];
		// 删除文章
		$article = Factory::M('ArticleModel');
		$result = $article->realDelArtById($art_id);
		//结果判断跳转
		if($result){
			$this->jump('index.php?p=Back&c=Article&a=recycle');	
		}else{
			$this->jump('index.php?p=Back&c=Article&a=recycle',':(发生未知错误 彻底删除文章失败');	
		}
		
	}
	/**
        * 回收站文章批量删除
        **/
	public function realDelAllAction(){
		
		// 先判断用户是否选择了文章
		if(!isset($_POST['art_id'])) {
			// 说明没有没有选择文章
			$this->jump('index.php?p=Back&c=Article&a=recycle', ':( 请先选择彻底要删除的文章!');
		}
		// 接受传来的数组,并处理成sql可以懂的字符串类型
		$art_ids = implode(',',$_POST['art_id']);
		
		// 调用模型 逻辑删除数据
		$article = Factory::M('ArticleModel');
		$result = $article->realDelAllArt($art_ids);
		if($result){
			$this->jump("index.php?p=Back&c=Article&a=recycle");	
		}else{
			$this->jump("index.php?p=Back&c=Article&a=recycle",':(发生未知错误 文章彻底批量删除失败');	
		}
		
	}
	/**
	 * 文章推荐的动作 ifRecommendAction()
	 */
	public function ifRecommendAction() {
		// 接收文章编号
		$art_id = $_GET['art_id'];
		// 接收推荐状态
		$is_recommend = $_GET['is_recommend'];
		// 接收当前页码
		$pageNum = $_GET['pageNum'];
		// 调用模型
		$article = Factory::M('ArticleModel');
		$result = $article->updateRecommendById($art_id, $is_recommend);
		if($result) {
			$this->jump("index.php?p=Back&c=Article&a=index&pageNum=$pageNum");
		}else {
			$this->jump("index.php?p=Back&c=Article&a=index&pageNum=$pageNum", '发生未知错误,推荐文章失败!');
		}
	}
	
}
?>

2.5.1 批量删除的时候, 上传的是一个文章id数组。

 <!-- 上端选择按钮 -->
<input type="button" class="button button-small checkall" name="checkall" checkfor="art_id[]" value="全选" />
<!-- 下端供选择的数据 -->
<td><input type="checkbox" name="art_id[]" value="{$row.art_id}" /></td>

2.5.2 Jquery绑定按钮触发事件,用于按钮点击后的页面跳转。

<script>
	//定义页面载入事件
	$(function(){
		//获取btnAdd按钮
		$('#btnAdd').bind('click',function(){
			// 设置“添加文章”链接
			window.location.href = 'index.php?p=Back&c=Article&a=add';
		});
	});
</script>

2.6 Platform - p, Controller - c, Action - a 分发参数

在请求前端控制器index.php的时候,get方法 向其传递p,c,a参数!决定p 平台(前台 or 后台)的c 控制器(一个功能聚合 例如文章控制器有 文章展示 文章添加 文章删除 文章回收站等功能),控制器的 a(细分操作动作 文章展示)

  • 比如:
    index.php?p=Back&c=Article&a=add
    后台管理页面 文章控制器 添加文章动作 结果是显示文章添加的页面

2.7 基础类

基础控制器类 Controller.class.php,基础模型类 Model.class.php的核心就是提供相应类的公共代码。

2.8 自动加载函数

有三种类需要进行加载:

  1. 框架核心类(已经确定好了)在Frame目录里 如MySQLDB.class.php, Controller.class.php, Model.class.php 等
  2. 控制器类(可以增加)
  3. 模型类(可以增加)

对于所有的类,可以分成两个方面来考虑:

  1. 对于已经确定好了的类,最好采用最简洁的方式进行直接加载!
  2. 对于不确定的可以增加的类,需要通过类名的规律,完成其位置的判断,然后再进行自动的加载!

其基本规律是:

  • 对于以Controller结尾的类,说明是控制器类,应该在当前对应的平台下的Controller目录进行载入!
  • 对于以Model结尾的类,说明是模型类,应该在当前对应的平台下的Model下进行载入!

2.8.1 spl_aotoload_register()

Frame.class.php 中的 initAotoload(),用于自动加载类函数,碰见不认识的类就调用这个方法。

分为两个步骤

  1. 自动加载函数 public static function aotoload($class_name)
  2. 注册自动加载函数
    2.1 spl_autoload_register(array(\CLASS, 'autoload'));
    2.2 spl_aotoload_register('self::autoload');
// Frame.class.php 中的 initAotoload()
	 // 自动加载函数
	public static function aotoload($class_name){
		//先把框架核心类放到数组里
		$frame_class_list =  array(
			//类名 => 类文件地址
			'Controller' => FRAME_DIR.'Controller.class.php',
			'Model'   => FRAME_DIR.'Model.class.php',
			'Factory' => FRAME_DIR.'Factory.class.php',
			'MySQLDB' => DAO_DIR.'MySQLDB.class.php',
			'PDODB'   => DAO_DIR.'PDODB.class.php',
			'I_DAO'	  => DAO_DIR.'I_DAO.interface.php',
			'Smarty'  => SMARTY_DIR.'Smarty.class.php',
			'Captcha' => VENDER_DIR.'Captcha.class.php',
			'Upload'  => FRAME_DIR.'Upload.class.php',
			'Page'    => FRAME_DIR.'Page.class.php',
			'Image'   => FRAME_DIR.'Image.class.php'
		);
		if(isset($frame_class_list[$class_name])){
			include $frame_class_list[$class_name];
		}elseif(substr($class_name,-10) == 'Controller'){
			include_once CURRENT_CON_DIR.$class_name.'.class.php';
		}elseif(substr($class_name,-5) == 'Model'){
			include_once CURRENT_MODEL_DIR.$class_name.'.class.php';
		}
	
    }
	private static function initAotoload(){
                // 注册自动加载函数
		spl_autoload_register('self::aotoload');
	}
public static function autoload($class_name){/*   */} //类外需要调用 所以用public
private static function initAutoload() {
		spl_autoload_register(array(__CLASS__, 'autoload'));
		spl_aotoload_register('self::autoload');
	}

php中文网 aotoload

对于各种文件的include ,本质是对类文件的包含,用于实列化类对象,所以可以用 spl_aotoload_register('aotoload'),aotoload是自己定义的类文件地址include,在脚本找不到 或者 接口时,会触发自定义的include。

CSDN 咔咔- :spl_aotoload_refister的前世今生

2.9 目录布局

其实2.8自动注册的时候已经用到了目录布局的结果。

在web的任何一级目录中,千万不能出现 中文

Apache访问时要严格的区分大小写,IIS不区分。

  • web根目录
    • APP 应用程序文件
      • Back 后台
        • Controller
          • AdminController.class.php
          • ArticleController.class.php
          • CategoryController.class.php
          • ManageController.class.php
          • MasterController.class.php
          • PlatformController.class.php
          • SinglePageController.class.php
        • Model
          • AdminModel.class.php
          • ArticleModel.class.php
          • CategoryModel.class.php
          • MasterModel.class.php
          • SinglePageModel.class.php
        • Public 存放ckeditor
          • ckeidtor
          • ckfinder
          • .htaccess Deny from all
        • View 以控制器名为子文件存放 视图
          • Admin
            • login.html
          • Article
            • add.html
            • edit.html
            • index.html
            • recycle.html
              *略
        • View_c smarty对视图的编译文件 基础控制器里写了这个编译的路径
      • Config
        • conf.php return一个数组 在应用中全局变量接收
      • Home
        • Controller
        • Model
        • View
        • View_c
      • .htaccess Deny from all
    • Frame 框架核心类文件
      *Dao
      • I_DAO.interface.php
      • MySQL.class.php
      • PDODB.class.php
      • Frame.class.php
      • Controller.class.php
      • Model.class.php
      • Page.class.php
      • Upload.class.php
      • Image.class.php
      • Factory.class.php
    • test 测试目录 完成项目后需要删除
    • Uploads 图片上传的存放目录
    • Vender 插件目录
    • index.php 入口文件

2.10 各大厂商的主要语言

一些基础的类中已经把复杂的操作封装成函数,但我们在使用时,又会将这些变简单的函数调用,又封装成一个新的函数。套娃也,这就是一花一世界。

Bilibili 开始用php,后来中台用Node,因为后台技术是为了更高的并发,更稳健,以及为了大数据分析,走过java.最后逐步换成 GO.

百度新浪是php

豆瓣是Py

bilibili是GO

2.11 创建框架初始化类

如果没有框架初始化类,入口文件 index.php 中会有大量的代码,不好看。我们将入口文件的各个操作解耦,编写框架初始化类。

2.11.1 Frame.class.php

<?php
/*
* 框架初始化类
*/
class Frame{
	
	/*
	项目入口方法
	*/
	public static function run(){
		// 定义基础目录常量 最初一级 web根目录下的第一级
		static::initConst();
		// 初始化配置
		static::initConfig();
		// 确定参数分发
		static::initDispatchParam();
		// 确定当前 平台 的模型,试图,控制器目录常量
		static::initPlatformConst();
		// 注册自动加载
		static::initAotoload();
		// 参数拼接方法
		static::initDispatch();
	}
	/**
	 * 定义基础目录常量
	 */
	 private static function initConst(){
		 
		define('ROOT_DIR',str_replace('\\', '/', getCWD() . '/')); // 根目录
		define('APP_DIR',ROOT_DIR.'App/'); //应用程序目录
		define('FRAME_DIR',ROOT_DIR.'Frame/');  //框架目录
		define('CONFIG_DIR',APP_DIR.'Config/'); // 配置文件目录
		define('DAO_DIR',FRAME_DIR.'Dao/'); //  dao层目录
		define('VENDER_DIR',ROOT_DIR.'Vender/') ;//插件目录
		define('SMARTY_DIR',VENDER_DIR.'Smarty/');//Smarty目录
		define('PUBLIC_DIR',ROOT_DIR.'Public/'); // Public 目录
		define('UPLOADS_DIR',ROOT_DIR.'Uploads/'); // 用户上传文件
	 }
	 
	 // 初始化配置
	 
	 private static function initConfig(){
		 $GLOBALS['conf'] = include_once CONFIG_DIR.'conf.php';
	 }
	 
	 // 确定参数分发
	 
	 private static function initDispatchParam(){
		 //确定平台分发参数p
		$default_platform = $GLOBALS['conf']['App']['default_platform'];
		define('PLATFORM',isset($_GET['p']) ? $_GET['p'] : $default_platform);


		// 确定分发参数c 控制器
		$default_controller = $GLOBALS['conf'][PLATFORM]['default_controller'];
		define('CONTROLLER',isset($_GET['c']) ? $_GET['c'] : $default_controller);

		// 确定分发参数A动作
		$default_action = $GLOBALS['conf'][PLATFORM]['default_action'];
		define('ACTION',isset($_GET['a']) ? $_GET['a'] : $default_action);
	 }
	 
	 // 确定当前 平台 的模型,试图,控制器目录常量
	 private static function initPlatformConst(){
		 /**
		 * 定义当前平台相关的目录常量
		 */
		define('CURRENT_CON_DIR',APP_DIR.PLATFORM.'/Controller/');
		define('CURRENT_MODEL_DIR',APP_DIR.PLATFORM.'/Model/');
		define('CURRENT_VIEW_DIR',APP_DIR.PLATFORM.'/View/');
		// 定义当前平台的 css js img
		define('CSS_DIR', './Public/' . PLATFORM . '/css');
		define('JS_DIR', './Public/' . PLATFORM . '/js');
		define('IMAGES_DIR', './Public/' . PLATFORM . '/images');
	 }
	 
	 // 注册自动加载
	public static function aotoload($class_name){
		//先把框架核心类放到数组里
		$frame_class_list =  array(
			//类名 => 类文件地址
			'Controller' => FRAME_DIR.'Controller.class.php',
			'Model'   => FRAME_DIR.'Model.class.php',
			'Factory' => FRAME_DIR.'Factory.class.php',
			'MySQLDB' => DAO_DIR.'MySQLDB.class.php',
			'PDODB'   => DAO_DIR.'PDODB.class.php',
			'I_DAO'	  => DAO_DIR.'I_DAO.interface.php',
			'Smarty'  => SMARTY_DIR.'Smarty.class.php',
			'Captcha' => VENDER_DIR.'Captcha.class.php',
			'Upload'  => FRAME_DIR.'Upload.class.php',
			'Page'    => FRAME_DIR.'Page.class.php',
			'Image'   => FRAME_DIR.'Image.class.php'
		);
		if(isset($frame_class_list[$class_name])){
			include $frame_class_list[$class_name];
		}elseif(substr($class_name,-10) == 'Controller'){
			include_once CURRENT_CON_DIR.$class_name.'.class.php';
		}elseif(substr($class_name,-5) == 'Model'){
			include_once CURRENT_MODEL_DIR.$class_name.'.class.php';
		}
	
        }

	private static function initAotoload(){
		spl_autoload_register('self::aotoload');
	}
	// 参数拼接方法
	private	static function initDispatch(){
		
		$controller_name = CONTROLLER.'Controller';
		$controller = new $controller_name; //   可变类
		// 先拼凑出当前方法的名字
		$action_name = ACTION.'Action';
		// 可变方法调用动作
		$controller->$action_name();
	} 
}	
?>

2.11.2 index.php

<?php

/*
前端控制器,入口文件,请求分发器
*/
include './Frame/Frame.class.php';

Frame::run();
?>

2.11.3 getCWD()

获得当前执行文件的绝对路径。

因为初始在phpStudey上是windows路径,而服务器是apache(linux),所以路径有改变。

原: getCWD(): str_replace('\','/',getCWD().'/');
localhost/PHP/PHP%20MVC/GetCWD.php C:\phpStudy\WWW\PHP\PHP MVC C:/phpStudy/WWW/PHP/PHP MVC/

2.12 创建配置文件

在一个项目中,有很多参数需要在很多的脚本中使用,比如数据库的相关参数,所以为了管理方便,应该建立相关的配置文件管理这些参数!

2.12.1 conf.php

<?php
return array(
	'db'=>array( // 数据库信息组 放到博客里时经过了处理 并非项目的信息配置
		'host'=>'127.0.0.1',
		'port'=>'3306',
		'user'=>'47_192_',
		'pass'=>'FELPi',
		'charset'=>'utf8',
		'dbname'=>'47_94_214_'
	),

	'App'=>array( // 应用程序组
		'default_platform'=>'Home',
		'dao' => 'pdo'  //pdo or mysql or mysqli
	),
	
	'Home'=>array( // 前台组
		'default_controller' => 'Index',
		'default_action' => 'index'
	),
	
	'Back'=>array( // 后台组
		'default_controller' => 'Admin',
		'default_action' => 'login'
	),
	
	
	'Captcha'	=>	array( // 验证码信息组
		'width'	=>	80,
		'height'=>	32,
		'pixelnum'=> 0.02,//干扰点密度
		'linenum' => 5,// 干扰线数量
		'stringnum'=> 4, // 验证码字符个数
	),

	'Page'	=>	array( // 分页信息组
		'rowsPerPage' =>3,//每页显示的记录数
		'maxNum' => 5  // 页面上能显示的最多有多少个页面
	)
	
	//其他

);
?>

2.13 数据库核心类

数据库核心类里有一个 interface接口控制文件。

由于项目需要,有MySQLDB.clss.php(只能接mysql) PDODDB.class.php(可接任何数据库)

因为网络带宽,我们需要对返回的查询信息进行分类。

  1. my_query($sql) 执行sql操作
  2. fetchAll 返回全部的信息 列表展示的信息
  3. fetchRow 返回一行信息 通常是确定的信息
  4. fetchColumn 返回一个信息 通常是数量

2.13.1 I_DAO.interface.php

<?php
interface I_DAO{
	
	public static function getInstance($config);
	public function my_query($sql);
	public function fetchAll($sql);
	public function fetchRow($sql);
	public function fetchColumn($sql);
}
?>

2.13.2 MySQLDB.class.php

<?php

/**
 * MySQLDB工具类
 */
class MySQLDB implements I_DAO{
	/**
	 * 定义相关的属性
	 */
	private $host; // 主机地址
	private $port; // 端口号
	private $user; // 用户名
	private $pass; // 密码
	private $charset; // 字符集
	private $dbname; // 数据库名
	// 运行的时候需要的属性
	private $link; // 保存连接资源
	private static $instance; // 用于保存对象
	/**
	 * 构造方法
	 */
	private function __construct($arr) {
		// 初始化属性的值
		$this->init($arr);
		// 连接数据库
		$this->my_connect();
		// 选择默认字符集
		$this->my_charset();
		// 选择默认数据库
		$this->my_dbname();
	}
	/**
	 * 获得单例对象的公开的静态方法
	 * @param array $arr 需要传递给构造方法的参数
	 */
	public static function getInstance($arr) {
		if(!self::$instance instanceof self) {
			self::$instance = new self($arr);
		}
		return self::$instance;
	}
	/**
	 * 初始化属性的值
	 */
	private function init($arr) {
		$this->host = isset($arr['host']) ? $arr['host'] : '127.0.0.1';
		$this->port = isset($arr['port']) ? $arr['port'] : '3306';
		$this->user = isset($arr['user']) ? $arr['user'] : 'root';
		$this->pass = isset($arr['pass']) ? $arr['pass'] : '';
		$this->charset = isset($arr['charset']) ? $arr['charset'] : 'utf8';
		$this->dbname = isset($arr['dbname']) ? $arr['dbname'] : '';
	}
	/**
	 * 连接数据库
	 */
	private function my_connect() {
		// 如果连接成功,就将连接资源保存到$link属性里面
		if($link = @mysql_connect("$this->host:$this->port",$this->user,$this->pass)) {
			$this->link = $link;
		}else {
			// 连接失败
			echo "数据库连接失败!<br />";
			echo "错误编号:", mysql_errno(), "<br />";
			echo "错误信息:", mysql_error(), '<br />';
			return false;
		}
	}
	/**
	 * 错误调试方法,执行一条sql语句
	 */
	public function my_query($sql) {
		$result = mysql_query($sql);
		if(!$result) {
			// 执行失败
			echo "SQL语句执行失败!<br />";
			echo "错误编号:", mysql_errno(), "<br />";
			echo "错误信息:", mysql_error(), '<br />';
			return false;	
		}
		return $result;
	}
	/**
	 * 返回多行多列的查询结果
	 * @param string $sql 一条sql语句
	 * @return mixed array|false
	 */
	public function fetchAll($sql) {
		// 先执行sql语句
		if($result = $this->my_query($sql)) {
			// 执行成功
			// 遍历资源结果集
			$rows = array();
			while($row = mysql_fetch_assoc($result)) {
				$rows[] = $row;
			}
			// 释放结果集资源
			mysql_free_result($result);
			// 返回所有的数据
			return $rows;
		}else {
			return false;
		}
	}
	/**
	 * 返回一行多列的查询结果
	 * @param string $sql 一条sql语句
	 * @return mixed array|false
	 */
	public function fetchRow($sql) {
		// 先执行sql语句
		if($result = $this->my_query($sql)) {
			// 执行成功
			$row = mysql_fetch_assoc($result);
			mysql_free_result($result);
			// 返回这一条记录的数据
			return $row;
		}else {
			return false;
		}
	}
	/**
	 * 返回单行单列的查询结果(单一值)
	 * @param string $sql 一条sql语句
	 * @return mixed string|false
	 */
	public function fetchColumn($sql) {
		// 先执行sql语句
		if($result = $this->my_query($sql)) {
			// 执行成功
			$row = mysql_fetch_row($result);
			// 释放结果集资源
			mysql_free_result($result);
			// 返回这个单一值
			return isset($row[0]) ? $row[0] : false;
		}else {
			// 执行失败
			return false;
		}
	}
	/**
	 * 选择默认的字符集
	 */
	private function my_charset() {
		$sql = "set names $this->charset";
		$this->my_query($sql);
	}
	/**
	 * 选择默认的数据库
	 */
	private function my_dbname() {
		$sql = "use $this->dbname";
		$this->my_query($sql);
	}
	/**
	 * 析构方法
	 */
	public function __destruct() {
		// 释放额外的数据库连接资源
		mysql_close($this->link);
	}
	/**
	 * __sleep方法,序列化对象的时候自动调用
	 */
	public function __sleep() {
		// 返回一个数组,数组内的元素为需要被序列化的属性名的集合
		return array('host', 'port', 'user', 'pass', 'charset', 'dbname');
	}
	/**
	 * __wakeup方法,反序列化一个对象的时候自动调用
	 */
	public function __wakeup() {
		// 数据库的相关初始化操作
		// 连接数据库
		$this->my_connect();
		// 选择默认字符集
		$this->my_charset();
		// 选择默认数据库
		$this->my_dbname();
	}
	/**
	 * 私有化克隆魔术方法,防止通过克隆得到新对象
	 */
	private function __clone() {

	}
	public function __set($name, $value) {

	}
	public function __get($name) {

	}
	public function __unset($name) {
		// 什么都不做,表示不能删除任何属性
	}
	public function __isset($name) {
		
	}
}

2.13.3 PDODB.class.php

<?php

class PDODB implements I_DAO{
	
	private $host; // 主机地址
	private $port; // 端口号
	private $user; // 用户名
	private $pass; // 密码
	private $charset; // 字符集
	private $dbname; // 数据库名
	
	private $dsn;  // 数据源的信息
	private $pdo;  // 存放PDO对象
	private static $instance;  // 用于存放PDODB类的单例对象
	
	public function __construct($arr){
		
		// 初始化属性
		$this->initParams($arr);
		// 初始化数据源
		$this->initDsn();
		// 实例化PDO对象
		$this->initPdo();
		// 初始化PDO对象属性
		$this->initPdoAttribute();
	}
	
	public static function getInstance($arr){
		
		if(!self::$instance instanceof self){
			self::$instance = new self($arr);
		}
		return self::$instance;
	}
	
	// 初始化属性
	private function initParams($arr){
		
	$this->host = isset($arr['host']) ? $arr['host'] : '127.0.0.1';
	$this->port = isset($arr['port']) ? $arr['port'] : '3306';
	$this->user = isset($arr['user']) ? $arr['user'] : 'root';
	$this->pass = isset($arr['pass']) ? $arr['pass'] : '';
	$this->charset = isset($arr['charset']) ? $arr['charset'] : 'utf8';
	$this->dbname = isset($arr['dbname']) ? $arr['dbname'] : '';
	
	}
	
    // 初始化数据源 dsn
	private function initDsn(){
		
		$this->dsn = "mysql:host=$this->host;port=$this->port;dbname=$this->dbname;charset=$this->charset";
	}
	
	// 获得PDO实例
	private function initPdo(){
		
		
		try{
			$this->pdo = new PDO($this->dsn,$this->user,$this->pass);
		}
		catch(PDOException $e){
			echo 'SQL语句执行失败<br />';
			echo '错误的信息为:', $e->getmessage(), '<br />';
			echo '错误的代码为:', $e->getCode(), '<br />';
			echo '错误的脚本为:', $e->getFile(), '<br />';
			echo '错误的行号为:', $e->getLine(), '<br />';
			return false;
			
		}
	}
	
	/**
	 * 初始化PDO对象属性
	 */
	private function initPdoAttribute() {
		// 把错误模式修改为异常模式
		$this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
	}
	/**
	 * 输出异常信息
	 */
	private function my_error($e) {
		echo 'SQL语句执行失败<br />';
		echo '错误的信息为:', $e->getmessage(), '<br />';
		echo '错误的代码为:', $e->getCode(), '<br />';
		echo '错误的脚本为:', $e->getFile(), '<br />';
		echo '错误的行号为:', $e->getLine(), '<br />';
		return false;
	}
	/**
	 * my_query 执行一条sql语句
	 */
	public function my_query($sql) {
		try{
			$result = $this->pdo->exec($sql);
		}catch(PDOException $e) {
			$this->my_error($e);
		}
		return $result;
	}
	/**
	 * fetchAll 获取多行多列的信息
	 */
	public function fetchAll($sql) {
		try{
			$stmt = $this->pdo->query($sql);
			$result = $stmt->fetchAll(PDO::FETCH_ASSOC);
			// 释放资源(关闭光标)
			$stmt->closeCursor();
		}catch(PDOException $e) {
			$this->my_error($e);
		}
		return $result;
	}
	/**
	 * fetchRow 获取一行多列的信息
	 */
	public function fetchRow($sql) {
		try{
			$stmt = $this->pdo->query($sql);
			$result = $stmt->fetch(PDO::FETCH_ASSOC);
			// 释放资源(关闭光标)
			$stmt->closeCursor();
		}catch(PDOException $e) {
			$this->my_error($e);
		}
		return $result;
	}
	/**
	 * fetchColumn 获取单行单列的信息(单一值)
	 */
	public function fetchColumn($sql) {
		try{
			$stmt = $this->pdo->query($sql);
			$result = $stmt->fetchColumn();
			// 释放资源(关闭光标)
			$stmt->closeCursor();
		}catch(PDOException $e) {
			$this->my_error($e);
		}
		return $result;
	}
	/**
	 * 私有化克隆方法
	 */
	private function __clone() {
		
	}	
}

?>

2.14 引入Smarty

Smarty是属于外部提供的功能(插件),我们一般的做法是将其放到一个专门的目录里面,该目录可以命名为Vendor,也可以叫作Tools!

在GitHub上可以找到相应的项目,下载即可。

2.14.1 设置目录常量和自动加载

在Frame.class.php 的目录常量里定义 Smarty 目录常量

有了目录常量后就可以放到自动加载函数的核心类里了。

2.14.2 项目中应用 Smarty

因为几乎所有的视图都需要Smarty控制,所以把他放到基础控制器类里。

还要设置他的模板目录,和编译目录。

调用时,其他控制器继承平台控制器(PlatformController.class.php),平台控制器继承基础控制器(Controller.class.php)就可以获得Smarty类进行操作了。

其他控制器里面的Smarty 调用

// 分配变量
$this->samrty->assign('artInfo',$artInfo);
// 展示相应的视图文件
$this->smarty->displau('art_index.html');

2.14.3 Controller.class.php

<?php

class Controller{

	//定义一个属性,用来保存 smarty对象
	protected $Smarty;
	
	public function __construct(){
		$this->initCode();
		// 初始化Smarty
		$this->initSmarty();
	}
	
	protected function initCode(){
		header("Content-type:text/html;Charset=utf-8");
		
		// 解决 smarty 时间报错
		date_default_timezone_set("PRC");
	}
	
	protected function initSmarty(){
		// 实例化Smarty
		$this->smarty = new Smarty;
		// 设置模板路径
		$this->smarty->setTemplateDir(CURRENT_VIEW_DIR . CONTROLLER . '/');
		// 设置编译文件路径
		$this->smarty->setCompileDir(APP_DIR . PLATFORM . '/View_c/' . CONTROLLER . '/');
	}
	
	/**
	 * 跳转
	 * @param $url 目标URL
	 * @param $info 提示信息
	 * @param $time 等待时间(单位秒)
	 */
	
	protected function jump($url, $info=null, $time = 10 ){
		
		if (is_null($info)){
			// 立即跳转
			header('Location:'.$url);
			die;
		}else{
			// 说明是刷新跳转,应该给出提示
			// 直接利用定界符输出模板
			echo <<<JUMP
		 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
			<title>提示信息</title>
			<style type='text/css'>
				* {margin:0; padding:0;}
				div {width:390px; height:287px; border:1px #09C solid; position:absolute; left:50%; margin-left:-195px; top:10%;}
				div h2 {width:100%; height:30px; line-height:30px; background-color:#09C; font-size:14px; color:#FFF; text-indent:10px;}
				div p {height:120px; line-height:120px; text-align:center;}
				div p strong {font-size:26px;}
			</style>
			<div>
				<h2>提示信息</h2>
				<p>
					<strong>$info</strong><br />
					页面在<span id="second">$time</span>秒后会自动跳转,或点击<a id="tiao" href="$url">立即跳转</a>
				</p>
			</div>
			<script type="text/javascript">
				var url = document.getElementById('tiao').href;
				function daoshu(){
					var scd = document.getElementById('second');
					var time = --scd.innerHTML;
					if(time<=0){
						window.location.href = url;
						clearInterval(mytime);
					}
				}
				// 以 1s 为间隔,不断的调用 daoshu() 函数,同时返回一个 mytime 标记值, 用clearInterval(mytime)来终结调用函数。
				var mytime = setInterval("daoshu()",1000); //
			</script>
JUMP;
			die;
		}
	}
	//  过滤数据,防止注入和一些非法数据的录入,因为和多地方要用,就写在基础控制器里
	protected function filterData($data){
		return addslashes(strip_tags(trim($data)));
	}
}
?>

2.14.4 art_index.html 后台文章首页的一个视图

{}包裹的即是变化的数据。

访问常量{$smarty.const.JS_DIR}

{include file='../Public/Public.html'}
<script>
	//定义页面载入事件
	$(function(){
		//获取btnAdd按钮
		$('#btnAdd').bind('click',function(){
			// 设置“添加文章”链接
			window.location.href = 'index.php?p=Back&c=Article&a=add';
		});
	});
	//定义页面载入事件
	$(function(){
		//获取butRecycle按钮
		$('#butRecycle').bind('click',function(){
			// 设置“回收站”链接
			window.location.href = 'index.php?p=Back&c=Article&a=recycle';
		});
	});
</script>
<div class="admin">
	<form action="index.php?p=Back&c=Article&a=delAll" method="POST">
    <div class="panel admin-panel">
    	<div class="panel-head"><strong>文章列表</strong></div>
        <div class="padding border-bottom">
            <input type="button" class="button button-small checkall" name="checkall" checkfor="art_id[]" value="全选" />
            <input type="button" id="btnAdd" class="button button-small border-green" value="添加文章" />
            <input type="submit" class="button button-small border-yellow"  onclick="return confirm('确认全部删除么?')"  value="批量删除" />
            <input type="button" id="butRecycle" class="button button-small border-blue" value="回收站" />
        </div>
        <table class="table table-hover">
        	<tr>
                <th width="45">选择</th>
                <th width="120">所属类别</th>
                <th width="200">文章标题</th>
                <th width="120">点击率</th>
                <th width="180">发布时间</th>
				<th width="180">推荐展示</th>
                <th width="100">操作</th>
            </tr>
			{foreach from=$artInfo item='row'}
            <tr>
                <td><input type="checkbox" name="art_id[]" value="{$row.art_id}" /></td>
                <td>{$row.cate_name}</td>
                <td>{$row.title|truncate:15}</td>
                <td>{$row.hits}</td>
                <td>{$row.add_time|date_format:'%Y-%m-%d %H:%M:%S'}</td>
				<td>
                    {if $row.is_recommend == '1'}
                    <a class="button border-yellow button-little" href="index.php?p=Back&c=Article&a=ifRecommend&art_id={$row.art_id}&is_recommend={$row.is_recommend}&pageNum={$smarty.get.pageNum|default:1}">已推荐</a> 
                    {else}
                    <a class="button border-blue button-little" href="index.php?p=Back&c=Article&a=ifRecommend&art_id={$row.art_id}&is_recommend={$row.is_recommend}&pageNum={$smarty.get.pageNum|default:1}">未推荐</a> 
                    {/if}
                </td>
                <td>
                    <a class="button border-blue button-little" href="index.php?p=Back&c=Article&a=edit&art_id={$row.art_id}">修改</a> 
                    <a class="button border-yellow button-little" href="index.php?p=Back&c=Article&a=del&art_id={$row.art_id}" onclick="return confirm('确认删除么?')" >删除</a>
                </td>
            </tr>
			{/foreach}
        </table>
		<div class="panel-foot text-center">
            {$strPage}
        </div>
    </div>
    </form>
    <br />
    <p class="text-right text-gray" style="float:right">基于<a class="text-gray" target="_blank" href="#">MVC框架</a>构建</p>
</div>
</body>
</html>

2.* 框架总结

访问网站时,默认访问 index.php。

在index.php 后以get方法传入平台,控制器,动作的参数。

调用 Frame.class.php。

框架类先定义基础目录常量,即网站的最基础一层的目录常量。以绝对路径进行定义,比较快速安全。

因为基础目录常量里定义了配置文件的目录,所以在这里进行初始化配置的行为。

接着确定参数分发,接收平台,控制器,动作的参数。只是确定了一个名字。不知道要去哪里调用他们。

所以跟着参数分发收到的平台参数。确定当前平台的控制器,模型,视图,css, js ,images等资源的路径

自动加载函数出现,根据当前的平台确定的路径,在遇到莫名其妙的类时,用include_once对应的类文件地址,把对应的类包含到程序中,就可以调用了。

参数拼接,用接收到的参数,拼接成完整的控制器类名,动作名,进行调用。 如:index.php?p=Back?c=Article&a=index 拼接成 ArticleController indexAction。先实例化控制器类,再调用拼接出来的动作。

实例化类时,先调用平台控制器构造函数,平台构造函数调用基础控制器,以获得公共的一些方法。

end接下来就是框架在实际应用中的应用了。

3 博客应用

3.1 初始化项目配置环境

3.1.1 搭建虚拟主机

用phpstudy 访问自己 的 网站,即在安装了phpstudy的机器里,在浏览器中直接输入自己定义的域名即可。

image

博客园 你的老张 PHPstudy配置

  1. 修改虚拟主机配置文件
  2. 重启Apache
  3. 创建根目录
  4. 增加hosts文件DNS解析

3.1.2 创建数据库

  • PphpStudy里有Mysql的管理平台,创一个数据库即可。
  • 在3宝塔linux中直接创建网站,就连带这创建了数据库。把他的数据库信息,填写到 conf.php 中就行了。数据库名,用户名,密码

3.1.3 其他配置

在网站一级目录下增加一个Public,用于存放前后台的css, js ,images等资源。并在Frame里进行配置。上面的Frame.class.php里有完整的配置。

3.2 实现后台登录功能

基本操作就是显示登陆表单,和封装一个验证码类,这个验证码类和表单绑定。点击一次,对相应的动作再次请求,把对应的正确验证码的字符串给放到session中。还有跳转动作(在Controller.class.php中实现),一些提示信息,页面跳转行为又他来管理。

验证信息后,用户信息记录到session并跳转到后台的管理页面,否则就给出提示并返回到登陆界面。

3.2.1 Captcha.class.php

<?php
/*
* 验证码工具类
*/

class Captcha{
	// 定义相关属性
	private $width;   // 宽
	private $height;  // 高
	private $pixelnum;// 干扰点密度
	private $linenum; // 干扰线数量
	private $stringnum; // 验证码字符的个数
	private $string;	// 要写入的随机字符串
	/**
	 * 构造方法
	 */
	public function __construct() {
		// 初始化相关属性
		$this->initParams();
	}
	/**
	 * 初始化相关属性
	 */
	private function initParams() {
		// 从配置文件中初始化属性
		$this->width = $GLOBALS['conf']['Captcha']['width'];
		$this->height = $GLOBALS['conf']['Captcha']['height'];
		$this->pixelnum = $GLOBALS['conf']['Captcha']['pixelnum'];
		$this->linenum = $GLOBALS['conf']['Captcha']['linenum'];
		$this->stringnum = $GLOBALS['conf']['Captcha']['stringnum'];
	}
	/**
	 * 生成验证码图片
	 */
	public function generate() {
		// 1, 创建画布
		$img = imagecreatetruecolor($this->width, $this->height);
		// 2, 填充背景
		// 2.1 创建背景色句柄
		$backcolor = imagecolorallocate($img,  mt_rand(200,255), mt_rand(150,255), mt_rand(200,255));
		// 2.2 填充背景
		imagefill($img, 0, 0, $backcolor);
		// 3, 得到随机验证码字符串
		$this->string = $this->getRandString();
		// 4, 验证码字符串写到图片上
		// 4.1 计算字符间隔
		$span = ceil($this->width/($this->stringnum + 1));
		// 4.2 循环写入单个字符
		for($i=1;$i<=$this->stringnum;$i++) {
			$stringcolor = imagecolorallocate($img, mt_rand(0,255), mt_rand(0,100), mt_rand(0,80));
			imagestring($img, 5, $i*$span, ($this->height/2)-6, $this->string[$i-1], $stringcolor);
		}
		// 5, 添加干扰线
		for($i=1;$i<=$this->linenum;$i++) {
			$linecolor = imagecolorallocate($img, mt_rand(0,150), mt_rand(30,250), mt_rand(200,255));
			$x1 = mt_rand(0, $this->width - 1);
			$y1 = mt_rand(0, $this->height - 1);
			$x2 = mt_rand(0, $this->width - 1);
			$y2 = mt_rand(0, $this->height - 1);
			imageline($img, $x1, $y1, $x2, $y2, $linecolor);
		}
		// 6, 添加干扰点
		for($i=1;$i<=$this->width*$this->height*$this->pixelnum;$i++) {
			$pixelcolor = imagecolorallocate($img, mt_rand(100,150), mt_rand(0,120), mt_rand(0,255));
			imagesetpixel($img, mt_rand(0,$this->width-1),mt_rand(0,$this->height-1), $pixelcolor);
		}
		// 7, 输出图片
		header("Content-type:image/png");
		ob_clean();// 清理数据缓冲区
		imagepng($img);
		// 8, 销毁图片
		imagedestroy($img);
	}
	/**
	 * 得到随机验证码字符串
	 */
	private function getRandString() {
		$arr = array_merge(range(0, 9), range('a', 'z'), range('A', 'Z'));
		shuffle($arr);
		$rand_keys = array_rand($arr, $this->stringnum);
		$str = '';
		foreach ($rand_keys as $value) {
			$str .= $arr[$value];
		}
		// 保存到session变量中
		@session_start();
		$_SESSION['captcha'] = $str;
		return $str;
	}
	/**
	 * 设置长和宽的公开方法
	 */
	public function setWidth($w) {
		$this->width = $w;
	}
	public function setHeight($h) {
		$this->height = $h;
	}
	/**
	 * 验证验证码是否合法的公开方法
	 */
	public function checkCaptcha($passcode) {
		@session_start();
		if(strtolower($passcode) !== strtolower($_SESSION['captcha'])) {
			return false;
		}else {
			return true;
		}
	}
}
?>

3.2.2表单调用验证码类

<div class="field">
                            <input type="text" class="input" name="passcode" placeholder="填写右侧的验证码" data-validate="required:请填写右侧的验证码" />
                            <img src="index.php?p=Back&c=Admin&a=captcha" onclick="this.src='index.php?p=Back&c=Admin&a=captcha&n='+Math.random()" width="80" height="32" class="passcode" />
                        </div>

3.2.3 跳转动作

PHP header()的7种用法

  • header('Location:'.$url);
    跳转到指定页面
    *header('content-type:text/html;charset=utf-8');
    声明content-type

3.3 防止用户FQ

此时,我们遇到了一个问题,如果用户不经过登录页面,直接访问后台管理页面,是可以访问到的。这个似乎和larvral的路由功能一样。

平台控制器横空出世 PlatformController.class.php.如果用户访问有登录权限的界面功能,这个控制器会查看是否有对方的session登录的信息,有就放行,没有就跳转到登录界面。不过这里有一个安全隐患,容易暴露后台登陆界面。

3.3.1 PlatformController.class.php

<?php

class PlatformController  extends Controller{
	
	
	public function __construct(){
		// 先显式的调用父类构造方法
		parent::__construct();
		
		$this->checkLogin();
		
	}
	
	public function checkLogin(){
		
		$no_need = array(
			// 控制器=>动作
			'Admin' => array('login','check','captcha')
		);
		
		if(isset($no_need[CONTROLLER]) && in_array(ACTION,$no_need[CONTROLLER])){
			//不需要验证的平台动作
			return;
		}
		// 判断登陆过与否
		@session_start();
		if(!isset($_SESSION['adminInfo'])){
			// 说明没有设置session 没有登陆过
			$this->jump('index.php?p=Back&c=Admin&a=login');
		}
	}
	
}
?>

3.4 防止SQL注入

  • 测试
    • 用户名:’ or 1 #
    • 用户名:‘ or 1 or ‘
    • 密码:随便写8位以上
    • 验证码:写正确
    • 结果登录上去了。
select * from bg_admin where admin_name='' or 1 #' and admin_pass=md5('123654uiykgjfhdsav')
select * from bg_admin where admin_name='' or 1
这两条是等价的,注释符改变了整个SQL语句的结构,肯定是能搜出数据的,那就可以登录了。

select * from bg_admin where admin_name='' or 1 or '' and admin_pass=md5('ewsdfgbnvb')
这个也可以搜出来

3.4.1 SQL注入的危害

不仅仅是在用户登录的时候,SQL语句可以被注入,其他任何用户的数据只要参与执行,都有可能被注入!

SQL注入的危害非常之大,有时候甚至可以删除服务器上的整个数据库:

  • 比如:用户名为:' or 1;drop database blog;#

3.4.2 SQL注入的防御

  1. 在业务逻辑上预防,比如要求用户名只能由特定的字符组成(比如数字字母下划线)(后面要学正则表达式)
  2. 使用PHP函数addslashes(最常用)
  3. 使用MySQL提供的数据转义函数:mysql_real_escape_string($data, $link);不过有一个前提是必须连接上数据库之后才可以使用!
  4. 使用预处理技术,因为预处理是强制将sql语句的结构和数据部分分开!

3.4.3 SQL注入防御用到的函数

  • trim():
    Strip whitespace (or other characters) from the beginning and end of a string
  • strip_tags():
    Strip HTML and PHP tags from a string

3.5 完成后台注销功能

public function logoutAction(){
		@session_start();
		// 清空对应变量的值
		unset($_SESSION['adminInfo']);
		// 是注销所有的session变量,并且结束session会话;
		session_destory(); //5.4.45不可用
		//跳转到的登陆页面
		$this->jump('index.php?p=Back&c=Admin&a=login');
	}

3.6 无限极分类

就是用数据库表来存储分类有子分类的数据,最后根据递归算法,把分类编程树状。子分类没有数量限制。

这个功能比较高级,很多应用的分类都是一级分类目录,没有子分类,比较简单。也好管理。

分类的编写是高于其他功能的编写的,比如文章功能的编写。

分类功能无非就是增删改查几个子功能

3.6.1 无限极分类代码实现

	/** CategoryModel.class.php
	* 格式化分类信息,并且树桩显示
	* @param $list 原始的sql二维表
	* @param int pid 父类id	
	* @param int level 树桩显示缩进级别
	**/
	private function getCateTree($list, $pid=0, $level=0){
		// 静态变量只初始化一次
		static $cate_tree = array();
		foreach ( $list as $row){
			if($row['cate_pid'] == $pid){
				// 把层级信息写入
				$row['level'] = $level;
				$cate_tree[] = $row;
				
				// 递归
				$this->getCateTree($list, $row['cate_id'], $level+1);
			}
			
		}
		return $cate_tree;
	}

3.6.2 批量删除分类

视图表单

<form method="post"  action="index.php?p=Back&c=Category&a=delAll">
<!-- checkfor="cate_id[]"选择cate_id[] -->
 <input type="button" class="button button-small checkall" name="checkall" checkfor="cate_id[]" value="全选" />
 
 <input type="submit" class="button button-small border-yellow" value="批量删除" />
 <!-- 选择小方框   name="cate_id[]" 变成了数组的参数-->
 <td><input type="checkbox" name="cate_id[]" value="{$row.cate_id}" /></td> 

模型

/**
* 批量删除模型的数据库操作
**/
	public function delAllCate($cate_id){
	
		
		// 此时$cate_id是一个数组,需要先转换为字符串
		$cate_id = implode(',', $cate_id);
		$sql = "delete from bg_category where cate_id in($cate_id)";
		return $this->dao->my_query($sql);
	}

3.6.3 把数组元素组合为一个字符串:implode()

<?php
$arr = array('Hello','World!','Beautiful','Day!');
echo implode(" ",$arr);
Hello World! Beautiful Day!
// 可以用来 in();
string(13) "1,2,4,5,9,6,7"
 
$cate_id = $_POST['cate_id'] ;// 控制器接受数据 这其实是一个数组类型的字符串
?>

3.7 文章管理

增删改查另加一个回收站功能,同时表中增加 is_del字段属性

  • is_del=‘1’ 在回收站显示。
  • is_del=‘0’ 文章页显示

3.8 文件添加功能

文章添加需要上传缩略图,而上传图片的时需要 enctype="multipart/form-data"

其他数据进行接受即可,并把他们放到一个数组里。在模型执行是用 ``extract($artInfo)``` 把关联数组变成一个一个的变量,进行insert操作。

enctype 属性规定在发送到服务器之前应该如何对表单数据进行编码。
默认地,表单数据会编码为 "application/x-www-form-urlencoded"。就是说,在发送到服务器之前,所有字符都会进行编码(空格转换为 "+" 加号,特殊符号转换为 ASCII HEX 值)。
enctype multipart/form-data 不对字符编码,在使用包含文件上传控件的表单时,必须使用该值。

<form method="POST" class="form-x" action="index.php?p=Back&c=Article&a=dealAdd" enctype="multipart/form-data">

truncate 英[trʌŋˈkeɪt] 美[ˈtrʌŋkeɪt]
v. 截短,缩短,删节(尤指掐头或去尾);

3.9 文件上传类

$_FILES:经由 HTTP POST 文件上传而提交至脚本的变量,类似于旧数组$HTTP_POST_FILES 数组(依然有效,但反对使用)详细信息可参阅 POST方法上传

天然就是一个二维数组
注:
  1. 文件被上传结束后,默认地被存储在了临时目录中,这时必须将它从临时目录中删除或移动到其它地方,如果没有,则会被删除。也就是不管是否上传成功,脚本执行完后临时目录里的文件肯定会被删除。所以在删除之前要用PHP的copy() 函数将它复制到其它位置,此时,才算完成了上传文件过程。

  2. 在 PHP 4.1.0 版本以前该数组的名称为 \(HTTP_POST_FILES,它并不像\)_FILES 一样是自动全局变量。PHP 3 不支持 $HTTP_POST_FILES数组。

  3. 用form上传文件时,一定要加上属性内容enctype="multipart/form-data",否则用$_FILES[filename]获取文件信息时会报异常。

3.9.1 Upload.class.php

<?php

/**
 * 文件上传类
 */
class Upload {
	// 定义公开静态属性,用于记录错误信息
	public static $error;
	/**
	 * 实现文件上传方法
	 * @param array $file 上传的文件的信息(一维数组,5个元素信息)
	 * @param array $allow 允许上传的类型
	 * @param string $path 文件上传的目录
	 * @param int $maxsize = 1048576 允许上传的文件的大小
	 * @return mixed false|$newname 上传失败返回false成功返回文件的新名字
	 */
	public function uploadAction($file, $allow, $path, $maxsize=1048576) {
		// 1,先判断系统错误
		switch($file['error']) {
			case 1 : self::$error = '上传失败!超出了文件限制的大小!'; 
					 return false;
			case 2 : self::$error = '上传失败!超出了浏览器规定的文件的大小!';
					 return false;
			case 3 : self::$error = '上传失败,文件上传不完整!';
					 return false;
			case 4 : self::$error = '上传失败,请先选择要上传的文件!';
					 return false;
			case 6 :
			case 7 : self::$error = '对不起,服务器繁忙,请稍后再试!';
					 return false;
		}
		// 2, 判断逻辑错误
		if($file['size'] > $maxsize) { 
			self::$error = "上传失败,超出了文件限制的大小!";
			return false;
		}
		if(!in_array($file['type'], $allow)) {
			// 非法的文件类型
			self::$error =  "上传的文件的类型不正确,允许的类型有:" . implode(',', $allow);
			return false;
		}
		// 3, 移动临时文件
		// 先得到该文件的随机名
		$newname = $this->randName($file['name']);
		$target = $path . '/' . $newname;
		// 开始移动
		$result = move_uploaded_file($file['tmp_name'], $target);
		if($result) {
			return $newname;
		}else {
			self::$error = "发生未知错误,上传失败!";
			return false;
		}
	}
	/**  
	 * 生成一个随机名字的方法
	 * @param string $filename 文件的原始名字
	 * @return string $newname 文件的新名字
	 */ 
	private function randName($filename) {
		// 1, 生成文件的时间部分
		$newname = date('YmdHis');
		// 2, 加上随机产生的6位数
		$str = "0987654321";
		for($i=0; $i<6; $i++) {
			$newname .= $str[mt_rand(0,strlen($str)-1)];
		}
		// 3, 加上文件的后缀名
		$newname .= strrchr($filename, '.');
		return $newname;
	}
}

3.10 Ckeditor 使用

就是富文本编辑器,使在web里写文章就像在word里写文章一样简单。缺点是不可以使用html标签,不是那么好用。

分为 ckeditor ckfinder 两个文件。放到 Back\Public里。进行两者简单的连接配置后,用一个js代码选择对应的textarea就可以吧文本域变为编辑器。

ckfinder用好之后就可以上传图片到服务器里了。这期间还有许多相关的配置,包括保存上传文件的文件路径等,这里不再赘述。

3.10.1 ckeditor ,ckfinder的连接

在ckedittor的配置文件 config.js 中 加入连接的配置代码。放到最后就好了。

// 载入CKfinder
	config.filebrowserBrowseUrl = '/App/Back/Public/ckfinder/ckfinder.html'; 
	config.filebrowserImageBrowseUrl = '/App/Back/Public/ckfinder/ckfinder.html?Type=Images';  
	config.filebrowserFlashBrowseUrl = '/App/Back/Public/ckfinder/ckfinder.html?Type=Flash'; 
	config.filebrowserUploadUrl = '/App/Back/Public/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files'; 
	config.filebrowserImageUploadUrl = '/App/Back/Public/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images'; 
	config.filebrowserFlashUploadUrl = '/App/Back/Public/ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Flash';
	

3.10.2 ckeditor的应用

01.将 textarea的 class设置为 ckeidtor

<!DOCTYPE html>
<html>
<head>
	<title>CKeditor在线编辑器</title>
	<meta charset="utf-8">
	<script type="text/javascript" src='./ckeditor/ckeditor.js'></script>
</head>
<body>
	<textarea class="ckeditor" name="content"></textarea>
</body>
</html>

02.利用 id="name",通过 ckeidtor.replace('name'),选中文本域。

<!DOCTYPE html>
<html>
<head>
	<title>CKeditor在线编辑器</title>
	<meta charset="utf-8">
	<script type="text/javascript" src='./ckeditor/ckeditor.js'></script>
</head>
<body>
	<textarea id="ck" name="content"></textarea>
	<script>
		CKEDITOR.replace('ck');
	</script>
</body>
</html>
  1. config.js 设置编辑器的颜色
// set color A0EEE1
config.uiColor = '#A0EEE1';
  1. 设置自己的客户端 myConfig.js来代替默认的配置信息,保存原始的配置信息。

复制config.js一份,改名为myConfig.js

CKEDITOR.replace('ck',{customConfig:'./myConfig.js'});

  1. 连接 ckfinder
    在myConfig.js 复制代码
// 载入CKfinder
	config.filebrowserBrowseUrl = './ckfinder/ckfinder.html'; 
	config.filebrowserImageBrowseUrl = './ckfinder/ckfinder.html?Type=Images';  
	config.filebrowserFlashBrowseUrl = './ckfinder/ckfinder.html?Type=Flash'; 
	config.filebrowserUploadUrl = './ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Files'; 
	config.filebrowserImageUploadUrl = './ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Images'; 
	config.filebrowserFlashUploadUrl = './ckfinder/core/connector/php/connector.php?command=QuickUpload&type=Flash';

3.11 封装分页类

分页类的作用就是,根据传来的页码,返回一个链接标签数组

<div class="page"><a href='index.php?p=Home&c=Index&a=index&pageNum=1'>首页</a>&nbsp<a href='index.php?p=Home&c=Index&a=index&pageNum=1'><font color='green'>1</font></a>&nbsp;<a href='index.php?p=Home&c=Index&a=index&pageNum=2'>2</a>&nbsp;<a href='index.php?p=Home&c=Index&a=index&pageNum=3'>3</a>&nbsp;<a href='index.php?p=Home&c=Index&a=index&pageNum=2'>下一页>></a><a href='index.php?p=Home&c=Index&a=index&pageNum=3'>尾页</a>&nbsp| 总页数:3</div>

最重要的是对分页类的应用。接受页码,根据每页显示的条数,总共的页数,当前页数,

在链接里加上页码,每次请求页码所对应的页面,调用相应的控制器,进行$offset的计算。比如是第三页,我们要通过offset去查询对应的数据
offset = (页码-1)每页数量。
例子:
这是第三页,每页有7个数据。offset=2
7=14
limit offset,每页数量 (14,7) 查询15-21这七条数据。
mysql> SELECT * FROM table LIMIT 5,10; // 检索记录行 6-15

最后调用分页类返回新的页码字符串在底部更新页码。

3.11.1 Page.class.php

其中一个生成一个随机名字的方法值得一看。

<?php

/**
 * 封装分页类
 */
class Page {
	// 定义相关属性
	private $rowsPerPage; // 每页显示的记录数
	private $maxNum; // 页面显示的最大页码数
	private $rowCount; // 总记录数
	private $url; // 固定url链接
	/**
	 * 构造方法,初始化相关属性
	 */
	public function __construct($rowsPerPage, $rowCount, $maxNum, $url) {
		$this->rowsPerPage = $rowsPerPage;
		$this->rowCount = $rowCount;
		$this->maxNum = $maxNum;
		$this->url = $url;
	}
	/**
	 * 核心方法,返回页码字符串
	 */
	public function getStrPage() {
		// 计算总页数
		$pages = ceil($this->rowCount / $this->rowsPerPage);
		// 确定当前选中的页码数
		$pageNum = isset($_GET['pageNum']) ? $_GET['pageNum'] : 1;
		// 定义页码字符串
		$strPage = '';
		// 拼凑出“首页”
		$strPage .= "<a href='{$this->url}pageNum=1'>首页</a>&nbsp";
		// 拼凑出“上一页”
		$preNum = $pageNum - 1;
		if($pageNum != 1) {
			$strPage .= "<a href='{$this->url}pageNum=$preNum'><<上一页</a>";
		}
		// 确定显示的初始页$startNum
		if($pageNum <= ceil($this->maxNum / 2)) {
			$startNum = 1;
		}else {
			$startNum = $pageNum - ceil($this->maxNum / 2) + 1;
		}
		// 确定显示初始页的最大值
		if($startNum >= $pages - $this->maxNum + 1) {
			$startNum = $pages - $this->maxNum + 1;
		}
		// 防止页面出现负值
		if($startNum <= 1) {
			$startNum = 1;
		}
		// 确定显示的最后一页$endNum
		$endNum = $startNum + $this->maxNum - 1;
		// 防止显示的最后一页越界
		if($endNum >= $pages) {
			$endNum = $pages;
		}
		// 拼凑出中间的页码
		for($i=$startNum;$i<=$endNum;$i++) {
			// 选中的当前页标红
			if($i == $pageNum) {
				$strPage .= "<a href='{$this->url}pageNum=$i'><font color='green'>$i</font></a>" . '&nbsp;';
			}else {
				$strPage .= "<a href='{$this->url}pageNum=$i'>$i</a>" . '&nbsp;';
			}
		}
		// 拼凑出“下一页”
		$nextNum = $pageNum + 1;
		if($pageNum != $pages) {
			$strPage .= "<a href='{$this->url}pageNum=$nextNum'>下一页>></a>";
		}
		// 拼凑出“尾页”
		$strPage .= "<a href='{$this->url}pageNum=$pages'>尾页</a>&nbsp";
		// 拼凑出总页码数
		$strPage .= "| 总页数:$pages";
		// 返回页码字符串
		return $strPage;
	}
}

3.12 文章编辑

在文章编辑和修改的时候,get参数的数字段要加上{},这是在控制器里的跳转代码,不加的话会报错,因为是数组类型。

"index.php?p=Back&c=Article&a=edit&art_id={$art['art_id']}"

进行文章编辑时,首先要从数据库里取出相应的数据,然后把数据放到表单里。通过value="XXXX",进行预放入。这样用户看到的就是待修改的原来的信息。

3.13 前台展示

前台展示包括导航栏,最新文章显示,分页,侧栏信息。

侧栏信息包括展示文章(推荐文章),展示点击(推荐文章的点击排行),百度分享,站长信息,和备案信息(固定的)。

导航栏是所有前台页面所必须的,所以放到前台的平台控制器里去实现分类查询和变量的分配。

侧栏,导航栏,备案信息可以分别做成单个html文件,这个文件的内容不需要完整的html的形式,把他们原来在视图html的形式剪贴出来,调用的时候用smarty去在主视图里包含就可以了。

3.13.1 PlatformController.class.php 前台

<?php
/**
 * 前台的平台控制器
 * 全平台通用的 title meta数据 一级标题栏都在这里初始化
 * 所有的controller都继承与他
 **/
class PlatformController extends Controller{
	
	public function __construct(){
	    // 先执行父类构造方法
	    parent::__construct();
	    // 初始化title meta数据
	    $this->initVars();
	    // 初始化一级导航栏
	    $this->initFirstCateInfo();
	    // 开启session
	    $this->initSession();
	}
	
	private function initFirstCateInfo(){
	    // 获取一级分类数据
	    $cate = Factory::M('CategoryModel');
	   	$firstCate = $cate->getFirstCate();
		$this->smarty->assign('firstCate',$firstCate);
	     
	}
	
    private function initVars(){
        
        $title = "Dba_sys的博客论坛";
        $kewwords = "个人博客模板,博客模板,响应式";
        $description = "php博客升级为论坛。";
        // 分配变量
        $this->smarty->assign('title',$title);
        $this->smarty->assign('kewwords',$kewwords);
        $this->smarty->assign('description',$description);
    }
    
    private function initSession(){
        @session_start();
    }
}
?>

3.13.2 功能拆分和视图中的文件包含


3.14 百度分享

百度分享网站似乎已经不在了,但是还好,模板文件里的分享还能用。

3.14.1 Smarty {literal}

用于包裹含有大括号的js 代码,因为存在大括号就要被smarty解析为变量等等,所以用这个包裹起来,说明是字面量。

3.14.2 百度分享html代码

    <div class="bdsharebuttonbox">
		<a href="#" class="bds_qzone" data-cmd="qzone" title="分享到QQ空间"></a>
		<a href="#" class="bds_tsina" data-cmd="tsina" title="分享到新浪微博"></a>
		<a href="#" class="bds_tqq" data-cmd="tqq" title="分享到腾讯微博"></a>
		<a href="#" class="bds_renren" data-cmd="renren" title="分享到人人网"></a>
		<a href="#" class="bds_weixin" data-cmd="weixin" title="分享到微信"></a>
		<a href="#" class="bds_more" data-cmd="more"></a>
	</div>

3.14.3 百度分享js代码

smarty中对于其他存在{}的代码要用{literal}{/literal}包裹

{literal}
<script>
window._bd_share_config={"common":{"bdSnsKey":{},"bdText":"","bdMini":"1","bdMiniList":false,"bdPic":"","bdStyle":"1","bdSize":"32"},"share":{}};with(document)0[(getElementsByTagName('head')[0]||body).appendChild(createElement('script')).src='http://bdimg.share.baidu.com/static/api/js/share.js?v=89860593.js?cdnversion='+~(-new Date()/36e5)];
</script>
{/literal}

3.15 面包屑导航

就是通过递归来找到当前栏目(分类)的父级分类,然后存储到数组中,从子分类开始向上递归,得到的数据追加到数组末端,然后倒置数组信息显示。

3.15.1 面包屑导航数据库操作

	/**
	 * 面包屑导航 ArticleModel.class.php
	 */
    public function getAllCateName($cate_id) {
		// 先获取所有的子分类号
		 $sql = "select cate_pid, cate_name from bg_category where cate_id=$cate_id";
		 $cateInfo = $this->dao->fetchRow($sql);
		 $cate_name = $cateInfo['cate_name'];
		 static $list = array();
		 $list[$cate_id] = $cate_name;
		 $cate_pid = $cateInfo['cate_pid'];
		 // 如果它的父类 pid 不为零,即它还有父亲, 我们就要递归找它的父亲,并把信息放到 $list里
		 if($cate_pid != 0){
		     $this->getAllCateName($cate_pid);
		 }
		 // 以不修改 $list[$cate_id]的方式逆转数组 ,如果要将逆转的数组自增排序 即从零开始 ture 改为false 或不写
		 return array_reverse($list,ture);
		 
	}

3.15.3 面包屑导航前端代码

  <!-- 面包屑导航 -->
    <h2 class="about_h">
            您现在的位置是:
            <a href="index.php?p=Home&c=Index&a=index">首页</a>
            {foreach from=$list item='row' key='key'}
            <a href="index.php?p=Home&c=Article&a=index&cate_id={$key}"> {$row} </a>
            {/foreach}
    </h2>

3.16 完成浏览次数

浏览次数的实现是每次点击文章,数据库取出文章数据的时候,顺带更新点击字段,点击字段加一。

3.17 上一篇|下一篇功能

上一篇的文章的查询语句就应该是:
select * from bg_article where is_del=’0’ and cate_id=8 and where art_id < 10 order by art_id desc limit 1;
下一篇的文章的查询语句就应该是:
select * from bg_article where is_del=’0’ and cate_id=8 and where art_id > 10 order by art_id  limit 1;

3.18 单页面

像关于我,联系我等看似永远改变的信息页面,在项目中是把相关信息放到了数据库里,后台管理人员在后台,用单页管理就像文章管理一样,也可以改变内容,而且比较方便。SinglePageController.class.php

3.19 评论功能升级到论坛

评论功能要求要注册,基于这个,个人博客的项目就可以升级为论坛。

要评论首先需要登录注册,登陆后将用户信息保存到session中。然后就像网站后台管理一样,可以做一个用户个人中心管理。用户可以发评论,就可以发自己的文章。

做用户的文章管理,评论管理,个人信息管理。

3.20 缩略图

用php把用户上传的原图制作成缩略图存放,使网站的界面更好看,更有规范。因为缩略图可以控制大小,我们可以固定缩略图的大小。

同时缩略图的存储更小,加快网站反应速度。

缺点是清晰度不够。

3.20.1 Image.class.php

<?php

class Image{
    // 错误信息变量
    public static $error;
    /**
     * @param int $max_w 
     * @param int $max_h
     * @param string $src_file 缩略图的地址路径类似 /Uploads/User/default.jpg
     * @param string $path 缩略图的输出路径
     * 缩略图背景色
     * @param int $red = 255
     * @param int $green = 255
     * @param int $blue = 255
     * 
     * @param bool|string(false|$thumb 缩略图名称)
     * */
    public function makeThumb($max_w, $max_h, $src_file, $path, $red=255, $green=255, $blue=255){
        // 1. 判断原图像是否存在 是否是图像文件而不是其他文件
        if(!file_exists($src_file)){
            self::$error = "原图像不存在";
            return false;
        }
        /** getimagesize()
         *  若不是图像返回 False 
         *  若是图像返回一个数组   []
         *  [0]->int 图片宽度 [1]-> int 图片高度
         *  [2]->int 图片类型 其中1为gif格式,2为jpg/jpeg格式,3为png格式
         * */
        if(!getimagesize($src_file)){
            self::$error = "原文件并非图像";
            return false;
        }
        
        // 2. 创建原图像画布,作为系统处理缩略图函数的参数
        //    根据原图片的不同 调用可变函数创建不同类型的图片画布
        $src_info = getimagesize($src_file);
        switch($src_info[2]){
            case 1: $type = "gif"; break;
            case 2: $type = "jpeg"; break;
            case 3: $type = "png"; break;
        }
        /** imagecreatefromjpeg()
         * 以上述函数为原型
         * 拼接可变函数
         * */
        $creat_image = 'imagecreatefrom'.$type;
        $src_img = $creat_image($src_file);
        
        // 3. 创建缩略图画布,并填充背景色
        $dst_img = imagecreatetruecolor($max_w, $max_h);
        $bgColor = imagecolorallocate($dst_img, $red, $green, $blue);
        imagefill($dst_img, 0, 0, $bgColor);
        
        // 4. 缩略图相关参数计算 怎么把大图片缩放到 缩略图的画布上 
        $dst_wh = $max_w / $max_h ; // 缩略图的宽高比 决定是宽比较长还是高比较长
        $src_w = $src_info[0];
        $src_h = $src_info[1];
        $src_wh = $src_w / $src_h;  // 原图的宽高比
        
        // 5. 确定拷贝到缩略图上的宽度和高度 
        if($src_wh > $dst_wh)   // 说明原图的宽度(写字台)比缩略图的宽度(冰箱)在比例上大 宽度作为定量把写字台缩放到冰箱里
        {
            $dst_w = $max_w;
            $dst_h = floor($dst_w / $src_wh); // 向下取整 向上也可以 两者比例相同时的一个简单运算
        }else{
            $dst_h = $max_h;
            $dst_w = floor($dst_h * $src_wh) ;
        }
        
        // 6. 确定拷贝到缩略图上的 x, y 坐标 左上角为原点
        $dst_x = ($max_w - $dst_w) / 2;
        $dst_y = ($max_h - $dst_h) / 2;
        
        // 7. 调用采样拷贝函数 
        if(imagecopyresampled($dst_img, $src_img, $dst_x, $dst_y, $src_x, $src_y, $dst_w, $dst_h, $src_w, $src_h)){
           // 采样成功 从原图片路径中获取原图片名字 
           $filename = basename($src_file);
           $thumb = 'th'.$filename; // 拼凑出缩略图的名字
           
           // imagejpg 保存对应类型的图片 到相应的地址中
           $save_image = 'image'.$type;
           // 清晰度调节 100是最高了 图片很差劲
           $save_image($dst_img, $path.'/'.$thumb, 100);
           
           //销毁画布资源
           imagedestroy($dst_img);
           imagedestroy($src_img);
           
           return $thumb;
            
        }else{
            //销毁画布资源
           imagedestroy($dst_img);
           imagedestroy($src_img);
           self::$error = "发生未知错误,缩略图生成失败";
           return false;
        }
    }
     
}

?>

4 零碎的知识点

4.1 instanceof

使用 PHP 中的 instanceof 运算符,可以判断一个对象是否属于某一个类,语法格式如下

<?php
    /*C语言中文网*/
    class A{
    }
    class B{
    }
    $obj = new A;
    var_dump($obj instanceof A);   //bool(true)
    echo '<br>';
    var_dump($obj instanceof B);  //bool(false)
?>

4.2 $rows[] = $row;

<?php 
$rows = array();
$row = array('key1.1'=>5,'key1.2'=>6);
echo var_dump($row);
$rows[] = $row;
echo var_dump($rows);
$row = array('key2.1'=>5,'key2.2'=>6);
$rows[] = $row; 
echo var_dump($rows);
?>
array(2) {
  ["key1.1"]=>
  int(5)
  ["key1.2"]=>
  int(6)
}
array(1) {
  [0]=>
  array(2) {
    ["key1.1"]=>
    int(5)
    ["key1.2"]=>
    int(6)
  }
}
array(2) {
  [0]=>
  array(2) {
    ["key1.1"]=>
    int(5)
    ["key1.2"]=>
    int(6)
  }
  [1]=>
  array(2) {
    ["key2.1"]=>
    int(5)
    ["key2.2"]=>
    int(6)
  }
}

4.3 MySql函数(WsCschool)

4.3.1 mysql_fetch_assoc(data)

mysql_fetch_assoc() 函数从结果集中取得一行作为关联数组。

data 必需。要使用的数据指针。该数据指针是从 mysql_query() 返回的结果。
  • 索引数组 - 带有数字索引的数组
<?php
$cars=array("Volvo","BMW","Toyota");
echo "I like " . $cars[0] . ", " . $cars[1] . " and " . $cars[2] . ".";
?>
  • 关联数组 - 带有指定键的数组 associative array 美 [ə'səʊʃiətɪv]
<?php
$age=array("Bill"=>"60","Steve"=>"56","Mark"=>"31");
echo "Bill is " . $age['Bill'] . " years old.";
?>

eg

<?php
$con = mysql_connect("localhost", "hello", "321");
if (!$con)
  {
  die('Could not connect: ' . mysql_error());
  }

$db_selected = mysql_select_db("test_db",$con);
$sql = "SELECT * from Person WHERE Lastname='Adams'";
$result = mysql_query($sql,$con);
print_r(mysql_fetch_assoc($result));

mysql_close($con);
?>
Array
(
[LastName] => Adams
[FirstName] => John
[City] => London
) 

4.3.2 mysql_fetch_row(data)

mysql_fetch_row() 函数从结果集中取得一行作为数字数组

data 必需。要使用的数据指针。该数据指针是从 mysql_query() 返回的结果。
eg
<?php
$con = mysql_connect("localhost", "hello", "321");
if (!$con)
  {
  die('Could not connect: ' . mysql_error());
  }

$db_selected = mysql_select_db("test_db",$con);
$sql = "SELECT * from Person WHERE Lastname='Adams'";
$result = mysql_query($sql,$con);
print_r(mysql_fetch_row($result));

mysql_close($con);
?>
Array
(
[0] => Adams
[1] => John
[2] => London
)

4.3.3 mysql_query(query,connection)

mysql_query() 函数执行一条 MySQL 查询。

参数 描述
query 必需。 规定要发送的 SQL 查询。注释:查询字符串不应以分号结束。
connection 可选。 规定 SQL 连接标识符。如果未规定,则使用上一个打开的连接。

mysql_query() 仅对 SELECT,SHOW,EXPLAIN 或 DESCRIBE 语句返回一个资源标识符,如果查询执行不正确则返回 FALSE。
对于其它类型的 SQL 语句,mysql_query() 在执行成功时返回 TRUE,出错时返回 FALSE。

结语

感谢所有分享知识的人!周洋老师,水巷先生。

posted @ 2021-07-15 22:08  Dba_sys  阅读(247)  评论(0编辑  收藏  举报