1、mvc原理
# mvc 与框架
## 1.mvc 是什么
1. m:model,模型(即数据来源),主要是针对数据库操作
2. v:view,视图,html 页面。视图由一个一个模板构成(模板是视图的一个具体展现或载体,视图是模板的一个抽象)
3. c:controller,控制器,用于mv之间的数据交互
## 2.最简单的 mvc
就是一个可以显示数据库内容的模板
## 3.分层后的mvc
### 2.1 控制器(以下三个)
1. 接受请求: 路由
2. 选择模型: CURD
3. 加载视图: 模板
### 2.2 模型(功能是操作数据库)
1. 查询构造器
2. 模型操作
## 2.3 视图(以下两个)
1. 模板赋值
2. 渲染视图
2、mvc的极简实现方式(一个页面)
<?php
//!数据库查询(model)
//第一步:连接数据库
$db = new PDO('mysql:dbname=phpedu','root','root');
// 第二步:对数据库进行查询
$stmt = $db->prepare('SELECT * FROM `staff` LIMIT ?');
// 第三步:数据绑定,获取指定的数据,确定获取的数据条数
$stmt->bindValue(1,5,PDO::PARAM_INT);
// 第四步:执行上述操作,如果不进行数据绑定就需要在execute里面加入参数
$stmt->execute();
// 第五步:把查询到的数据集保存到一个变量里备用
$staffs = $stmt->fetchAll(PDO::FETCH_ASSOC);
?>
<!-- 视图(view) -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>员工列表</title>
</head>
<body>
<!-- 第六步:foreach渲染数据 -->
<h3>员工列表</h3>
<?php foreach ($staffs as $staff) :extract($staff) ?>
<li>
<?=$id?>:<?=$name?> , <?=$sex ? '女':'男'?>(<?=$email?>)
</li>
<?php endforeach?>
</body>
</html>
上述代码中,模型model和视图view都已经存在了,而控制器controller实际上已经有了,但是我们看不到,因为我们上面的业务逻辑太简单,直接把数据写进去了。没有通过controller让model和view进行数据交互。
3、分层的mvc
先写控制器controller,控制器写完写模型model,模型写完最后再写视图view。
其中,core里面包含模型类(Model.php),视图类(View.php),控制器类(controller.php)
Model.php
<?php
// 模型类
// 命名空间遵循psr-4
// 类名与文件名同名
// 当前类的命名空间与当前类所在的路径应该是一一对应的
namespace core;
use PDO;
// 视图,控制器和模型不能直接使用,因为这是底层的业务逻辑
// 是框架源码的一部分,不要让用户直接用,因为框架一旦更新
// 新的框架源码会把core目录下的所有代码全部覆盖
// 所以视图,控制器和模型里面的内容和类尽量把它们转为抽象的
// 或者干脆写个接口,让用户来进行实例化
// 模型抽象化(每个用户一张表),只允许通过子类使用
abstract class Model
{
// 要把连接对象写成属性,因为这个对象它会在当前的模型类中多个方法中使用
protected $db = null;
// 1.连接数据库
// 在实例化时能够自动连接,可以写在构造函数里
public function __construct($dsn,$username,$password)
{
$this->db = new PDO($dsn,$username,$password);
}
// 2.内置一些基本的底层操作,供用户的自定义模型用
// 自定义模型:与某一个或某一张数据表相关的类
// 2.1获取全部数据
public function select($num)
{
$sql = 'SELECT *FROM `staff` LIMIT ?;';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1,$num,PDO::PARAM_INT);
// !这种方法可以确保当出错时我们知道错误在哪里
// if($stmt->execute()){
// return $stmt->fetchAll(PDO::FETCH_ASSOC);
// }else{
// print_r($stmt->errorInfo());
// }
// !也可以用简化版,因为出错概率较低
$stmt->execute();
return $stmt->fetchAll(PDO::FETCH_ASSOC);
}
// 2.2获取某个数据(id)
public function getOne($id)
{
$sql = 'SELECT *FROM `user` WHERE `id` = ?;';
$stmt = $this->db->prepare($sql);
$stmt->bindParam(1,$id,PDO::PARAM_INT);
$stmt->execute();
return $stmt->fetch(PDO::FETCH_ASSOC);
}
}
View.php
<?php
// 视图类
namespace core;
class view
{
// 1.模板变量容器
protected array $data = [];
// 2.模板赋值
public function assign(string $key,$value)
{
$this->data[$key] = $value;
}
// 3.渲染视图
// 渲染与传参可以同步完成
// $path告诉数据显示在哪个页面中
public function render(string $path,array $data = [])
{
if($data)
{
foreach($data as $key=>$value)
{
$this->assign($key,$value);
}
}
// 将模板变量数组展开为独立的变量,以方便传入到模板中使用
extract($this->data);
// 渲染/加载模板文件
file_exists($path) ? include $path : die('模板不存在');
}
}
Controller.php
<?php
// 控制器类
namespace core;
abstract class Controller
{
// 1.模型对象
protected Model $model;
// 2.视图对象
protected View $view;
// 3.实例化,初始化上面的模型对象,视图对象
public function __construct(Model $model,View $view)
{
$this->model = $model;
$this->view = $view;
}
}
autoload.php(自动加载器)
<?php
// 注册类的自动加载器方法
spl_autoload_register(function($class){
// require str_replace('\\','/',$class) . 'php';
// 为了系统的兼容性,可以使用
require str_replace('\\',DIRECTORY_SEPARATOR,$class) . '.php';
// 这是一个可以代替 composer 的方法,参数是function,传入了一个类名,
// 然后用str_replace这个函数,把类里面的命名空间,也就是反斜线替换成路径符,
// 然后在后面加扩展名 .php 转变为类就行了
// 这样就实现了了一个类的自动加载了(相当于require了一个命名空间+类名)
});
如果想要把这些类一个一个加载到项目中去,则需要针对不同的业务类型创建不同的控制器和模型,所以要创建一个controller文件夹
StaffController.php(用户自定义控制器)
虽然不是控制器基类,但是要求必须继承自控制器基类(父类)
<?php
// 自定义控制器,必须继承自控制器基类(超类/父类)
namespace controller;
use core\Controller;
use core\Model;
use core\View;
use model\StaffModel;
class StaffController extends Controller
{
public function __construct(Model $model,View $view)
{
// 里面这样写就冗余了
// $this->model = $model;
// $this->view = $view;
//子类的构造函数直接调用父类的构造方法就可以了
parent::__construct($model,$view);
}
// 自定义方法:默认方法
// index():列出所有数据
public function index($num = 10)
{
// 1.选择模型:获取数据
$staff = $this->model->getAll($num);
// 2.加载视图
// 路径约定:view/控制器/方法名.php
// key值可以理解为变量名,相当于把$staff接收到的值赋给了名为staffs的变量
$this->view->render('view/staff/index.php',['staffs'=>$staff]);
}
}
创建类文件夹,里面都是自定义模型类
StaffModel.php(用户自定义模型类)
<?php
// 自定义模型
namespace model;
use core\Model;
class StaffModel extends Model
{
public function __construct($dsn,$username,$password)
{
parent::__construct($dsn,$username,$password);
}
// 获取全部数据
public function getAll($num)
{
return $this->select($num);
}
}
创建视图文件夹,在视图里再创建文件夹,staff文件夹对应是当前的控制器,里面的文件对应当前的方法。
staff>index.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>员工列表</title>
</head>
<body>
<!-- 第六步:foreach渲染数据 -->
<h3>员工列表</h3>
<?php foreach ($staffs as $staff) :extract($staff) ?>
<li>
<?=$id?>:<?=$name?> , <?=$sex ? '女':'男'?>(<?=$email?>)
</li>
<?php endforeach?>
</body>
</html>
在mvc2文件夹下创建测试文件index.php
mvc2>index.php
<?php
namespace mvc2;
use controller\StaffController;
use model\StaffModel;
use core\view;
// 入口文件:测试
// 1.类的自动加载器
// 自动加载器主要用来加载间接用到的类,但是直接用到的类还是需要用use
require __DIR__ . '/core/autoload.php';
// 路由解析
// 2.实例化控制器
$model = new StaffModel('mysql:dbname=phpedu','root','root');
$view = new View();
// 实例化控制器对象
$staff = new StaffController($model,$view);
// 3.调用控制器中的方法
$staff->index(3);
上述运行结果:
4、路由原理及应用
<?php
// 路由的本质:是从url中解析出控制器,控制方法,以及方法的参数
// 1.controller:控制器类名
// 2.method:控制器中的某个方法名
// 3.parameter:参数列表,以数组形式
// 以上三种数据在url中的展示方式有两种
// 1.queryString:查询字符串
// 2.PATH_INFO:路径信息
// phpedu.io/one/two/demo1.php?查询字符串,以键值对方式,和&分开
// !c:controller,m:method,p:parameter
// phpedu.io/one/two/demo1.php?c=hello&m=method&p=aaa
// 得到这个查询字符串以后,我们可以通过一些方法,将该字符串解析成数组,从而得到控制器,方法和参数
//! 在我们的脚本名称 `phpedu.io/one/two/demo1.php` 和查询字符串 `c=hello&m=method&p=aaa`之间
// !如果又出现路径,我们用 PATH_INFO 表示 /user/index/100/200
// phpedu.io/one/two/demo1.php PATH_INFO ?c=hello&m=method&p=aaa
// phpedu.io/one/two/demo1.php/user/index/100/200?c=hello&m=method&p=aaa
// /user/index/100/200:PATH_INFO
function p($data)
{
echo is_array($data) ? sprintf('<pre>%s</pre>',print_r($data,true)) : $data;
}
p([1,2,3]);
p('Hello');
echo '<hr>';
// !QueryString: 查询字符串
// 超全局数组$_SERVER的QUERY_STRING键可以返回当前的查询字符串
p($_SERVER['QUERY_STRING']);
// 将查询字符串解析到数组里面
parse_str($_SERVER['QUERY_STRING'],$request);
p($request);
// 人为认定
// c controller, m:method,name:parameter
$controller = array_shift($request);
$method = array_shift($request);
$params = array_shift($request);
// 控制器类:测试专用
class HelloController
{
public function world($name)
{
return 'Hello ,' . $name;
}
}
// 生成控制器类名
$controller = ucfirst($controller) . 'Controller';
echo (new $controller)->$method($params);
// 一般用回调的方式来调用
// echo call_user_func_array([new $controller(),$method],[$params]);
echo call_user_func([new $controller(),$method],$params);
echo '<hr>';
// ! 2.PATH_INFO:查询字符串与脚本之间的路径信息
//* http://phpedu.io/0824/router.php/hello/world/admin?c=hello&m=world&name=peter
//* PATH_INFO:/hello/world/admin
p($_SERVER['PATH_INFO']);
p(explode('/',$_SERVER['PATH_INFO']));
// 但这样之后发现索引0对应的值为空
// p(array_filter(explode('/',$_SERVER['PATH_INFO'])));
// 去除空字符也可以这样
$request = explode('/',trim($_SERVER['PATH_INFO'],'/'));
$controller = array_shift($request);
$method = array_shift($request);
$params = array_shift($request);
$controller = ucfirst($controller) . 'Controller';
echo call_user_func([new $controller(),$method],$params);
// 也可以 echo call_user_func_array([new $controller(),$method],[$params]);
// !推荐使用PATH_INFO
// 通过url重写功能,可以将脚本的扩展名php隐藏,也可以在末尾自定义一个扩展名
//* 隐藏后的地址具有欺骗性:http://phpedu.io/0824/router/hello/world/admin.html
/**
* 总结:
* 1.$_SERVER['QUERY_STRING']以键值对方式返回当前字符串,返回值是数组,需要用parse_str转换成字符串
* 2.1.$_SERVER['PATH_INFO']返回带有/的字符串,返回值是字符串,需要用explode切割转换成数组
*/
上述运行结果:
在mvc3文件夹下创建测试文件index.php
<?php
namespace mvc3;
use model\UserModel;
use core\view;
use core\Router;
// 入口文件:测试
// 1.类的自动加载器
require __DIR__ . '/core/autoload.php';
// 路由解析
$request = Router::parse();
$controller = array_shift($request);
$method = array_shift($request);
$params = array_shift($request);
// 生成控制器名称
$controller ='controller\\' . ucfirst($controller) . 'Controller';
// echo $controller;
// die;
// 2.实例化控制器
$model = new UserModel('mysql:dbname=phpedu','root','root');
$view = new View();
// 实例化控制器对象
$user = new $controller($model,$view);
// 3.调用控制器中的方法
call_user_func_array([$user,'get'],$params);
mvc3>core>router.php
<?php
namespace core;
// 路由器类
class Router
{
public static function parse(): array
{
// 默认控制器,实际项目,应该来自配置文件,而不是在写死
$controller = 'Index';
$action = 'index';
// 参数列表
$params = [];
// 判断是否存在pathinfo
if (array_key_exists('PATH_INFO', $_SERVER) && $_SERVER['PATH_INFO'] !== '/') {
// 为什么要判断 $_SERVER['PATH_INFO'] !== '/' ?
// 因为: admin.php/ 时,$_SERVER['PATH_INFO'] = '/', 导致解析控制器失败
$pathinfo = array_filter(explode('/', $_SERVER['PATH_INFO']));
// dump($pathinfo);
// 考虑到index.php/ 情况, 这时pathinfo为空数组
if (count($pathinfo) >= 2) {
$controller = array_shift($pathinfo);
$action = array_shift($pathinfo);
$params = $pathinfo;
// $params = array_shift($pathinfo);
} else {
$controller = array_shift($pathinfo);
}
}
// 查看控制器,方法,参数
// dump($controller, $action, $params);
// 将这些数据返回出去
return [$controller, $action, $params];
}
}
mvc3>model>UserModel
<?php
// 自定义模型
namespace model;
use core\Model;
class UserModel extends Model
{
public function __construct($dsn,$username,$password)
{
parent::__construct($dsn,$username,$password);
}
// 获取全部数据
public function get($id)
{
return $this->getOne($id);
}
}
mvc3>view>user>get
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>员工信息</title>
</head>
<body>
<!-- 第六步:foreach渲染数据 -->
<h3>员工信息</h3>
<?php if (is_array($user)) : ?>
<?php foreach ($user as $key => $value) : ?>
<!-- 如果不想拿 -->
<?php
// if($value = null){
// echo '该用户不存在';
// }
if ($key === 'password' || $key === 'register_time') {
continue;
}
?>
<li>
[<?= $key ?>] => <?= $value ?>
</li>
<?php endforeach ?>
<?php endif ?>
<?php if (is_array($user) == null) : ?>
<h3>此用户不存在</h3>
<?php endif ?>
</body>
</html>
上述运行结果: