MVC框架/服务容器/路由/门面技术综合实例
1. 创建库表和初始数据,代码结构
创建库表
初始数据(部分)
代码结构图
2. MVC各部分
- 2.1 控制器类
PlayerController.php
<?php
namespace controller;
use model\facade\PlayerModel;
use PDO;
class PlayerController {
/**
* 查询球员分页数据
* $p: 当前页码
* $r: 每页显示记录数
*/
public function players($p = 1, $r = 3) {
// 查询球员总数
$count = PlayerModel::playerCount();
// 调整异常页码数值
if(!filter_var($p, FILTER_VALIDATE_INT)){
$p = 1;
}
$p < 1 ? 1 : ($p > $count ? $count : $p);
if(!filter_var($r, FILTER_VALIDATE_INT)){
$p = 3;
}
// 查询分页数据
$players = PlayerModel::players($p, $r);
// 加载视图文件,渲染数据
require(dirname(__DIR__) . '/view/player/list.php');
}
// 跳转到球员信息编辑界面
public function edit($id) {
// 判断球员id有效性
if(!filter_var($id, FILTER_VALIDATE_INT, ['option' => ['min_range' => 1]])) {
echo("<script>alert('无效的参数');window.history.go(-1);</script>");
exit;
}
// 查询球员信息
$player = PlayerModel::player($id);
// 加载视图模板,并渲染数据
require(dirname(__DIR__) . '/view/player/edit.php');
}
// 保存修改
public function doUpdate() {
// post请求, 修改后的球员信息在其中
$player = $_POST;
// 执行保存
PlayerModel::savePlayer($player);
}
// 跳转到删除确认页面
public function del() {
require(dirname(__DIR__) . '/view/player/del.php');
}
// 执行删除球员
public function doDel() {
// 球员id
$id = filter_var($_GET['id'], FILTER_VALIDATE_INT, ['options' => ['min_range' => 1]]);
// 球员列表的分页参数
$pos = !empty($_GET['pos']) ? '?' . urldecode($_GET['pos']) : '';
// 无效的id,不处理
if(!$id) {
echobr("<script>alert('无效的参数');window.location.href='/player/index.php/player/players{$pos}'</script>");
}
// 执行删除
PlayerModel::delPlayer($id, $pos);
}
}
2.2 模型类
PlayerModel.php
- 模型类
\view\PlayerModel.php
- 模型类
<?php
namespace model;
use PDO;
class PlayerModel {
// PDO
protected $pdo = null;
public function __construct(PDO $pdo)
{
$this->pdo = $pdo;
}
// 分页查询球员信息
public function players($page = 1, $pageSize = 3) {
// 分页起始记录偏移量
$start = ($page - 1) * $pageSize;
$sql = "SELECT * FROM `player` LIMIT {$start}, {$pageSize}";
$players = $this->pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
return $players;
}
// 查询球员总数
public function playerCount() {
// 查询记录总数
$sql = 'SELECT count(`id`) as row_count FROM `player`';
$count = ($this->pdo->query($sql)->fetch(PDO::FETCH_NUM))[0] ?? 0;
return $count;
}
// 根据id查询某个球员信息
public function player($id) {
$sql = "SELECT * FROM `player` WHERE `id` = ?";
$stmt = $this->pdo->prepare($sql);
$stmt->execute([$id]);
// 查不到球员信息, 提示错误,不继续执行
if ($stmt->rowCount() !== 1) {
echo ("<script>alert('无效的ID值');window.history.go(-1);</script>");
die;
}
// 获取查到的球员信息
$player = $stmt->fetch(PDO::FETCH_ASSOC);
return $player;
}
// 保存修改
public function savePlayer($playerInfo) {
$sql = "UPDATE `player` SET `name` = :name, `team` = :team, `height` = :height, `weight` = :weight, `position` = :position, `update_time` = :update_time WHERE `id` = :id";
$param = $playerInfo;
// 更新时间=当前时间
$param['update_time'] = time();
// 球员分页信息(返回球员列表用)
$pos = $param['pos'];
unset($param['pos']);
$pos = empty($pos) ? '' : '?' . $pos;
// PDO执行更新
$stmt = $this->pdo->prepare($sql);
$stmt->execute($param);
// 验证更新结果
if ($stmt->rowCount() === 1) {
if (empty($pos))
echo ("<script>alert('修改成功');window.history.back();</script>");
else {
$pos = urldecode($pos);
echo ("<script>alert('修改成功');window.location='/player/index.php/player/players{$pos}';</script>");
}
} else {
echo ("<script>alert('修改失败');</script>");
}
}
// 根据球员id删除球员信息
public function delPlayer($id, $pos='') {
$sql = "DELETE FROM `player` WHERE `id` = :id";
$stmt = $this->pdo->prepare($sql);
$stmt->execute(['id' => $id]);
$pos = empty($pos) ? '' : '?' . $pos;
// 判断处理条数
if ($stmt->rowCount() === 1) {
echobr("<script>alert('删除成功');window.location.href='/player/index.php/player/players{$pos}'</script>");
} else {
echobr("更新失败");
printfpre($stmt->errorInfo());
echobr("<a href='/player/index.php/player/players{$pos}'>返回</a>");
}
}
}
- 模型门面类
\view\facade\PlayerModel.php
<?php
namespace model\facade;
use core\Facade;
class PlayerModel extends Facade {
}
- 门面类基类
\core\Facade.php
<?php
namespace core;
// 门面类基类
class Facade {
// 无法解决创建类实例时的参数传输
public static function __callStatic($name, $arguments) {
return Container::make(lcfirst(ltrim(lcfirst(strrchr(static::class, '\\')), '\\')))->$name(...$arguments);
}
}
- 2.3 视图文件
- 球员分页列表·list.php·
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>球员列表</title>
<style>
@import '/player/style/page_style.css';
@import '/player/style/page_style.css';
@import '/player/style/list.css';
</style>
</head>
<?php
// require('/pagination.php');
?>
<body>
<table cellspacing="0" align="center">
<caption>球员列表</caption>
<thead>
<tr>
<th>ID</th>
<th>姓名</th>
<th>球队</th>
<th>身高(cm)</th>
<th>体重(kg)</th>
<th>位置</th>
<th>创建时间</th>
<th>修改时间</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<?php if (!empty($players) && count($players) > 0) : ?>
<?php foreach ($players as $player) : ?>
<tr>
<td><?php echo $player['id']; ?></td>
<td><?php echo $player['name']; ?></td>
<td><?php echo $player['team']; ?></td>
<td><?php echo $player['height']; ?></td>
<td><?php echo $player['weight']; ?></td>
<td><?php echo $player['position']; ?></td>
<td><?php echo date('Y-m-d H:i:s', $player['create_time']); ?></td>
<td><?php echo date('Y-m-d H:i:s', $player['update_time']); ?></td>
<td>
<a href="/player/index.php/player/edit?id=<?php echo $player['id']; ?>&pos=<?php echo urlencode($_SERVER['QUERY_STRING']); ?>">修改</a>
<a href="/player/index.php/player/del?id=<?php echo $player['id']; ?>&name=<?php echo $player['name']; ?>&pos=<?php echo urlencode($_SERVER['QUERY_STRING']); ?>">删除</a>
</td>
</tr>
<?php endforeach; ?>
<?php else : ?>
<tr>
<td colspan="9">啥也没查到...</td>
</tr>
<?php endif ?>
</tbody>
</table>
<?php (new \view\Pagination($count, $p, $r, 5))->echoPagination();
?>
<?php //echo (new Pagination($count, $currentPage, 3, 5));
?>
</body>
</html>
- 修改球员信息界面
edit.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>修改球员信息</title>
<style>
@import url('/player/style/common.css');
@import url('/player/style/edit.css');
</style>
</head>
<body>
<section>
<div class="player-edit-header">修改球员信息</div>
<div class="player-info">
<form action="/player/index.php/player/doUpdate" method="post">
<input type="hidden" name="id" value="<?php echo $player['id']; ?>">
<input type="hidden" name="pos" value="<?php echo $_GET['pos']; ?>">
<div class="info-item">
<label for="name">姓名: </label>
<input type="text" name="name" id="name" value="<?php echo $player['name']; ?>" required autofocus>
</div>
<div class="info-item">
<label for="team">球队: </label>
<input type="text" name="team" id="team" value="<?php echo $player['team']; ?>" required>
</div>
<div class="info-item">
<label for="height">身高(cm): </label>
<input type="number" name="height" id="height" value="<?php echo $player['height']; ?>" required>
</div>
<div class="info-item">
<label for="weight">体重(kg): </label>
<input type="number" name="weight" id="weight" value="<?php echo $player['weight']; ?>" required>
</div>
<div class="info-item">
<label for="position">位置: </label>
<input type="text" name="position" id="position" value="<?php echo $player['position']; ?>" required>
</div>
<div class="info-item">
<button type="submit">保存</button>
</div>
</div>
</form>
</section>
</body>
</html>
- 删除球员信息
del.php
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>确认删除</title>
<style>
@import url('/player/style/common.css');
@import url('/player/style/del.css');
</style>
</head>
<body>
<section>
<span>确认要删除<strong><?php echo $_GET['name'] ?></strong>吗?</span>
<a href="/0512/player.php<?php echo $_GET['pos'] ? '?' . $_GET['pos'] : ''; ?>" class="btn">取 消</a>
<a href="do_del.php?id=<?php echo $_GET['id'] ?>&pos=<?php echo urlencode($_GET['pos']) ?>">确 认</a>
</section>
</body>
</html>
- 分页类
\view\Pagination.php
<?php
namespace view;
class Pagination
{
// 第一页
private $start = 1;
// 一页可容纳的记录数
private $pageSize = 3;
// 显示的页码个数
private $pageNumSize = 5;
// 显示的页码列表
private $pageNumList = [];
// 总页数
private $pageCount = 0;
// 页码左右偏移量
private $pageNumOffset = 0;
// 当前页码
private $currentPage = 1;
// 记录总数
private $rowCount = 0;
public function __construct(int $rowCount, int $currentPage = 1, int $pageSize = 3, int $pageNumSize = 5)
{
// 初始化各种属性
$this->rowCount = $rowCount;
$this->pageSize = $pageSize;
$this->pageNumSize = $pageNumSize;
$this->pageNumOffset = ($pageNumSize - 1) / 2;
$this->pageCount = ceil(floatval($rowCount) / $pageSize);
/* 当传入的当前页码效于最小页码时,初始化为1;大于最大页码时,初始化为最大页码 */
$this->currentPage = $currentPage < 1 ? 1 : ($currentPage > $this->pageCount ? $this->pageCount : $currentPage);
$this->getPageNumList();
}
/**
* 获取要显示的页码列表
*/
private function getPageNumList()
{
// 如果要显示的页码数量>=总页码数量,则显示所有页码。
if ($this->pageCount <= $this->pageNumSize) {
$this->pageNumList = range(1, $this->pageCount, 1);
return;
}
// 起始页码,取“当前页码-页码偏移量”和起始页码的最大值。
$pageNumStart = ($this->currentPage - $this->pageNumOffset) < $this->start ? $this->start : ($this->currentPage - $this->pageNumOffset);
// 结束页码,取“当前页码+页码偏移量”和总页码数的最小值。
$pageNumEnd = ($pageNumStart + $this->pageNumSize - 1) > $this->pageCount ? $this->pageCount : ($pageNumStart + $this->pageNumSize - 1);
// 若结束页码等于总页码,则再计算一次起始页码(避免当前页到结束页码的差值小于页码偏移量的情况)
if ($pageNumEnd === $this->pageCount) {
// 起始页码,取“最大页码-要显示的页码数量+1”和起始页码的最大值。
$pageNumStart = ($this->pageCount - $this->pageNumSize + 1) < $this->start ? $this->start : ($this->pageCount - $this->pageNumSize + 1);
}
// 生成要显示的页码数组
$this->pageNumList = range($pageNumStart, $pageNumEnd, 1);
}
// 拼接字符串,形成分页HTML代码字符串。
public function getPagination()
{
$pageHtml = '<div class="pagination">';
// 首页
$tmpHtml = $this->currentPage === 1 ? '<span class="pageNum disabled">首页</span>'
: sprintf('<a class="pageNum" href="%s?p=1&r=%d">首页</a>', $_SERVER['PHP_SELF'], $this->pageSize);
$pageHtml .= $tmpHtml;
// 上一页
$tmpHtml = $this->currentPage === 1 ? '<span class="pageNum disabled">上一页</span>'
: sprintf('<a class="pageNum" href="%s?p=%d&r=%d">上一页</a>', $_SERVER['PHP_SELF'], $this->currentPage - 1, $this->pageSize);
$pageHtml .= $tmpHtml;
// 间隔符
$tmpHtml = $this->pageNumList[0] >= 2 ? '...' : '';
$pageHtml .= $tmpHtml;
// 页码
foreach ($this->pageNumList as $pageNum) {
$tmpHtml = $pageNum == $this->currentPage ? sprintf('<span class="active">%d</span>', $pageNum)
: sprintf('<a class="pageNum {$pageNum}" href="%s?p=%d&r=%d">%d</a>', $_SERVER['PHP_SELF'], $pageNum, $this->pageSize, $pageNum);
$pageHtml .= $tmpHtml;
}
// 间隔符
$tmpHtml = $this->pageNumList[array_key_last($this->pageNumList)] + 1 <= $this->pageCount ? '...' : '';
$pageHtml .= $tmpHtml;
// 下一页
$tmpHtml = $this->currentPage >= $this->pageCount ? '<span class="pageNum disabled">下一页</span>'
: sprintf('<a class="pageNum" href="%s?p=%d&r=%d">下一页</a>', $_SERVER['PHP_SELF'], $this->currentPage + 1, $this->pageSize);
$pageHtml .= $tmpHtml;
// 末页
$tmpHtml = $this->currentPage >= $this->pageCount ? '<span class="pageNum disabled">末页</span>'
: sprintf('<a class="pageNum" href="%s?p=%d&r=%d">末页</a>', $_SERVER['PHP_SELF'], $this->pageCount, $this->pageSize);
$pageHtml .= $tmpHtml;
// 总页码
$pageHtml .= "<span>共{$this->pageCount}页</span>";
// 页码跳转表单
$tmpHtml = "<form action='{$_SERVER['PHP_SELF']}' method='get'> <input type='text' name='p'><input type='hidden' name='r' value='{$this->pageSize}'><button type='submit'>跳转</button></form>";
$pageHtml .= $tmpHtml;
$pageHtml .= '</div>';
return $pageHtml;
}
// 直接向浏览器输出分页信息
public function echoPagination()
{
echo $this->getPagination();
}
}
3. MVC框架运行类
- 3.1 文件自动加载器
autoload.php
<?php
# 自动加载函数
// 封装自动加载器
try {
// 系统函数: spl_autoload_register(), 把demo8中加载文件的代码复制过来
spl_autoload_register(function ($className) {
// 1. 将类名中的反斜线改为当前系统中的目录分隔符
$path = str_replace('\\', DIRECTORY_SEPARATOR, $className);
// echobr($path);
// 2. 生成真正要加载的类文件名称
/* autoload.php放到core目录中,所以需要用dirname()函数来获取上一级目录的路径 */
$file = dirname(__DIR__) . DIRECTORY_SEPARATOR . $path . '.php';
// echobr($file);
// 3. 加载这个文件
require $file;
});
} catch (Exception $e) {
die($e->getMessage());
}
- 3.2 服务容器类
\core\Container.php
<?php
namespace core;
// 服务容器类
class Container {
// 类实例容器
protected static $instances = [];
// 私有化构造方法
private function __construct(){
}
// 把类实例的创建闭包绑定到容器中(约定别名为小驼峰类名)
public static function bind($alias, \Closure $process) {
static::$instances[$alias] = $process;
}
// 从容器中获取类实例
public static function make($alias, array $param=[]) {
return isset($alias) ? static::$instances[$alias](...$param) : false;
}
// 销毁类实例
public static function destroy($alias) {
unset(static::$instances[$alias]);
}
}
- 3.3 路由解析类
\core\Route.php
<?php
namespace core;
class Route {
public static $config = [
'controller_suffix' => 'Controller',
'action_suffix' => '',
'view_suffix' => 'View',
'model_suffix' => 'Model',
];
/**
* 解析路由,约定:若存在路径信息,则以路径信息的前两位作为controller和action,剩下的路径信息参数跟查询参数合并作为action的
* 入参;若路径信息小于2,则action由查询参数的第一个键值对指定,剩下的查询参数作为action的入参;
*/
public static function analysis() {
// 加载配置文件
$config = require((dirname(__DIR__)) . '/config.php');
array_merge(static::$config, $config);
// 处理路径信息
$pathInfoParam = empty($_SERVER['PATH_INFO']) ? [] : static::analysisPathInfo($_SERVER['PATH_INFO']);
// 处理查询字符串
$queryStrParam = empty($_SERVER['QUERY_STRING']) ? [] : static::analysisQueryStr($_SERVER['QUERY_STRING']);
$reqInfo = $pathInfoParam;
// 路径信息和查询字符串解析结果中都没有控制器信息,则不再继续
if(!isset($reqInfo['controller']) && !isset($queryStrParam['c'])) {
echobr('无法处理的URL,未指定控制器');die;
}
// 否则,优先考虑使用路径信息中的控制器;最后再考虑使用请求参数中指定的控制器(参数名为c)
if(isset($reqInfo['controller'])) {
unset($queryStrParam['c']);
} else {
$reqInfo['controller'] = $queryStrParam['c'];
unset($queryStrParam['c']);
}
// 路径信息和查询字符串解析结果都没有方法信息,则不再继续
if(!isset($reqInfo['action']) && !isset($queryStrParam['c'])) {
echobr('无法处理的URL,未指定action');die;
}
// 否则,优先考虑使用路径信息中的方法,最后在考虑使用请求参数中指定的方法(参数名为a)
if(isset($reqInfo['action'])) {
unset($queryStrParam['a']);
} else {
$reqInfo['action'] = $queryStrParam['a'];
unset($queryStrParam['a']);
}
// 合并action的参数
if(count($queryStrParam) > 0) {
$reqInfo['param'] = array_merge($reqInfo['param'], $queryStrParam);
}
return $reqInfo;
}
// 解析路径信息
private static function analysisPathInfo($pathInfo) : array {
// 保存路径信息解析结果的数组
$pathInfoParam = ['param' => []];
// 切割字符串,形成数组
$pathInfoArr = explode('/', trim($pathInfo, '/'));
$paramCount = count($pathInfoArr);
// 解析出来,有值,那么弹出第一个作为控制器
if($paramCount > 0) {
$pathInfoParam['controller'] = lcfirst(array_shift($pathInfoArr)) . static::$config['controller_suffix'];
}
// 值数量超过1个,则再弹出第二个作为方法
if($paramCount > 1) {
$pathInfoParam['action'] = lcfirst(array_shift($pathInfoArr)) . static::$config['action_suffix'];
}
// 值数量超过3个,则剩下的拼成参数键值对
if($paramCount > 2) {
for($index = 0; $index < count($pathInfoArr); $index += 2) {
if(isset($pathInfoArr[$index + 1])) {
$param[$pathInfoArr[$index]] = $pathInfoArr[$index + 1];
}
}
$pathInfoParam['param'] = $param;
}
return $pathInfoParam;
}
// 解析查询参数
private static function analysisQueryStr($queryStr) {
// echobr($queryStr);
$queryStrParam = [];
parse_str($queryStr, $queryStrParam);
return $queryStrParam;
}
}
- 3.4 框架运行类
\core\Runner.php
<?php
namespace core;
use controller\PlayerController;
use model\PlayerModel;
use PDO;
class Runner {
private static $init = false;
public static function run() {
// 解析路由
$reqInfo = Route::analysis();
// 获取控制器
$controller = Container::make($reqInfo['controller']);
// 执行控制器方法
$action = $reqInfo['action'];
//echobr($action);
$controller->$action(...array_values($reqInfo['param']));
}
// 把创建类实例的闭包放入容器中
public static function init() {
if(self::$init) return;
// pdo
$alias = 'pdo';
Container::bind($alias, function() {
return new PDO('mysql:host=localhost;dbname=phpedu;charset=utf8;port=3306', 'root', 'root');
});
// 默认用类的小驼峰命名作为别名
$alias = lcfirst(ltrim(strrchr(PlayerModel::class, '\\'), '\\'));
Container::bind($alias, function () {
$pdo = Container::make('pdo');
return new PlayerModel($pdo);
});
// 控制器
$alias = lcfirst(ltrim(strrchr(PlayerController::class, '\\'), '\\'));
Container::bind($alias, function() {
return new PlayerController;
});
self::$init = true;
}
}
- 3.5 配置类
config.php
<?php
return [
'controller_suffix' => 'Controller',// 控制器后缀
'action_suffix' => '',// 方法后缀
'view_suffix' => 'View',// 视图后缀
'model_suffix' => 'Model',// 模型后缀
];
- 3.6 入口文件
index.php
<?php
use core\Runner;
require('../out.php');
require('core/autoload.php');
// 前端控制器
// 把创建类闭包放入容器
Runner::init();
// go! go! go!
Runner::run();
运行结果:
与查询分类作业一样: 查询分类作业链接
学习心得
把路由, 服务容器和MVC框架的作业用一个小实例实现了.
实现过程遇到的问题:
- 如何实现自动把创建类实例的闭包放入容器中(即Runner::init()方法如何自动实现)?
- 创建类实例的闭包, 如何判断参数所属类型,并在容器中找到并注入? 若参数顺序和构造方法的形参顺序不一致, 如何传参?
- 执行控制器的方法时, 如何判断参数所属类型,假设为对象,如何在容器中查找并注入? 若参数顺序和action方法的形参顺序不一致, 如何传参?