自撸 PHP 发开框架
1.MVC三层架构
- M :model, 使用第三方包实现
- V :view, 使用第三方包实现
- C :controller :业务逻辑是写在控制器中
2.下载第三方包
- Model :
composer require catfan/medoo
- View :
composer require league/plates
- 分别执行以上命令下载第三方模型和视图库文件包
{
"require": {
"catfan/medoo": "^2.0",
"league/plates": "^3.4"
},
"autoload": {
"psr-4": {
"app\\controllers\\": "app/controllers",
"app\\models\\": "app/models",
"app\\views\\": "app/views",
"core\\": "core"
}
}
}
https://packagist.org/packages/catfan/medoo#v2.0.0
https://packagist.org/packages/league/plates#v3.4.0
3. 构建使用流程
- 创建自己的框架核心代码,MODEL, VIEW,分别继承第三方的包
- 创建自己的应用, 按MVC架构模式,创建属于自己的models, views, controllers
core/Model.php
<?php
namespace core;
use Medoo\Medoo;
/**
* 继承第三方模型 atfan/medoo
*/
//框架核心模型类文件
class Model extends Medoo
{
public function __construct()
{
//mysql连接参数
$config = [
'database_type' => 'mysql',
'server' => 'localhost',
'database_name' => 'mydb',
'username' => 'root',
'password' => 'root'
];
// 给父类的构造方法传递参数
parent::__construct($config);
}
}
?>
core/View.php
<?php
namespace core;
use League\Plates\Engine;
/**
* 继承第三方视图 league/plates
*/
//框架核心 视图类
class View extends Engine
{
//不能起名叫directory, 会重写父类属性$directory;
public $template; // 所渲染模板的目录
// 可以传入第二个参数,指定渲染模板的文件后缀
public function __construct($path,$fileExtension='html')
{
$this->template = parent::__construct($path);
}
}
?>
一.MVC框架核心组成:
1.MVC框架大体可分为model、view、controller,这三块主要都是类、对象的应用和扩展
2.关于composer组件是对PHP代码模块化的一种形式,代码封装,再利用;
3.MVC框架整体构架没有变化,只是在MVC的架构中添加了composer组件管理器,来方便添加组件功能;
4.对于框架可以整体理解为三部分:MVC框架部分{app和core}和组件部分{vendor和composer.json、composer.lock}以及入口文件;
5.MVC框架重点理解:命名空间和自动加载之间的关系;往往出问题都在自动加载这块,自动加载出问题:往往跟命名空间、类名称和文件路径不一致导致;注意:\
和/
的使用场景
二. 实战案例MVC框架
(利用我们继承过来的模型catfan/medoo和视图league/plates写一个个MVC小框架)
创建的目录文件结构如下:
composer.json文件中自动加载配置项
{
"name": "ldy/frame",
"description": "MVC小框架",
"require": {
"catfan/medoo": "^1.7",
"league/plates": "^3.4"
},
"autoload": {
"psr-4": {
"app\\controllers\\": "app/controllers",
"app\\models\\": "app/models",
"app\\views\\": "app/views",
"core\\": "core"
}
}
}
执行命令:composer dump-autoload
- 控制类代码 app\controllers\UsersController.php
<?php
namespace app\controllers;
class UsersController
{
private $model=null;
private $view=null;
public function __construct($model,$view)
{
$this->model = $model;
$this->view = $view;
}
public function index(){
// return __METHOD__;
return $this->view->render('users/index',['name'=>'zhang']);
}
}
?>
- 模型类代码 app\models\UsersModel.php
<?php
// 业务层模型类
namespace app\models;
// 引入并继承核心模型文件
use core\Model;
class UsersModel extends Model
{
public function __construct(){
parent::__construct();
}
}
?>
- 视图类代码 app\views\users\index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<h1>hello world! </h1>
<h2>My name is <?=$name?></h2>
</body>
</html>
- 入口文件代码 public\index.php
<?php
// composer 自动加载器
require __DIR__.'\\..\\vendor\\autoload.php';
use app\controllers\UsersController;
use app\models\UsersModel;
use core\View;
// 测试业务层模型类 UsersModel
// var_dump(new UsersModel);
// 测试核心层 视图类 view
// var_dump(new View("../../app/views"));
// 测试控制器 UsersController
// echo (new UsersController($model,$view))->index();
$model = new UsersModel;
$view= new View("../app/views");
$controller = new UsersController($model,$view);
echo $controller->index();
?>
浏览地址:http://zhang.com/0520/zhang/public/index.php
可以通过设置网站根目录为0520/zhang/public这样预览地址:http://zhang.com/index.php
效果如下:
在入口文件中添加 .htaccess文件
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
这样浏览地址中就可以省略index.php 直接通过http://zhang.com/即可访问
三.封装动态分页案例
我们将之前用传统方式编写的动态分页案例改为MVC三层架构模式
模型层 models/StudentsModel.php
<?php
// 业务层模型类
namespace app\models;
// 引入并继承核心模型文件
use core\Model;
class StudentsModel extends Model
{
public function __construct(){
parent::__construct();
}
}
?>
视图层 views/students/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>
<link rel="stylesheet" href="/static/css/style.css">
</head>
<body>
<table>
<caption>学生信息表</caption>
<thead>
<tr>
<td>学号</td>
<td>姓名</td>
<td>性别</td>
<td>操作</td>
</tr>
</thead>
<tbody>
<?php foreach($students as $user):?>
<tr>
<td><?= $user['sno']?></td>
<td><?= $user['sname']?></td>
<td><?= $user['ssex']?></td>
<td><button>删除</button><button>编辑</button></td>
</tr>
<?php endforeach;?>
</tbody>
</table>
<?php
//讨论省略点分页 前提: 两边的省略点都出现的时候
//分页条显示的页数
$showPages = 5;
//分页条的开始页码值
$startPage = 1;
//分页条的结束页码值
$endPage = $pages;
//分页条的终止页码相对于当前页码的偏移量:
$offset = ($showPages-1)/2;
if($showPages < $pages)
{
if($page > $offset+1)
{
$startOmit = '...';
$startPage = $page-$offset;
$endPage = $page+$offset;
if($endPage > $pages){$endPage=$pages;}
}else{
$startPage = 1;
$endPage = $showPages;
}
if($showPages<$pages && $page + $offset < $pages)
$endOmit = '...';
}
?>
<!-- 动态生成分页条 跳转地址 当前页码的高亮显示 -->
<p class="page">
<!-- 首页 上一页 -->
<?php $prev=$page-1; if($page == 1) $prev = 1;?>
<a href="<?=$page != 1?'?p=1':'javascript:;'?>" style="<?=$page==1?'cursor: no-drop;':'' ?>">首页</a>
<a href="<?=$page != 1?'?p='.$prev:'javascript:;'?>" style="<?=$page==1?'cursor: no-drop;':'' ?>">上一页</a>
<?php if(isset($startOmit)):?>
<a href="javascript:;"><?=$startOmit?></a>
<?endif?>
<!-- 1 2 3 4 5 分页跳转页数字 -->
<?php for ($i=$startPage; $i <=$endPage ; $i++) :?>
<a class="<?=$i==$page?'active':''?>" href="<?='?p='.$i?>"><?=$i?></a>
<? endfor;?>
<?php if(isset($endOmit)):?>
<a href="javascript:;">...</a>
<?endif?>
<!-- 下一页 尾页 -->
<?php $next=$page+1; if($page == $pages) $next=$page;?>
<a href="<?=$page != $pages?'?p='.$next:'javascript:;'?>" style="<?=$page==$pages?'cursor: no-drop;':'' ?>">下一页</a>
<a href="<?=$page != $pages?'?p='.$pages:'javascript:;' ?>" style="<?=$page==$pages?'cursor: no-drop;':'' ?>">尾页</a>
</p>
</body>
</html>
控制层 controllers/StudentsController.php
<?php
namespace app\controllers;
class StudentsController
{
private $model=null;
private $view=null;
public function __construct($model,$view)
{
$this->model = $model;
$this->view = $view;
}
public function index(){
$num = 5;
$page = empty($_GET['p'])?1:$_GET['p'];
// 获取数据
$offset = ($page-1)*$num;
$students = $this->model->select('student',['sno','sname','ssex'],['LIMIT'=>[$offset,$num]]);
// 获取总页数
$total = $this->model->count('student');
$pages = ceil($total/$num);
return $this->view->render('students/index',['page'=>$page,'students'=>$students,'pages'=>$pages]);
}
}
?>
入口文件 public/index.html
<?php
// composer 自动加载器
require __DIR__.'\\..\\vendor\\autoload.php';
use app\controllers\StudentsController;
use app\models\StudentsModel;
use core\View;
$model = new StudentsModel;
$view= new View("../app/views");
$controller = new StudentsController($model,$view);
echo $controller->index();
?>
http://zhang.com/?p=6 这样显然不太合适,我们需要将参数改为http://zhang.com/p/6这种形式
控制器 controllers/StudentsController.php
<?php
namespace app\controllers;
class StudentsController
{
private $model=null;
private $view=null;
public function __construct($model,$view)
{
$this->model = $model;
$this->view = $view;
}
public function index($page=1,$num=3){
// $num = 5;
// 不在通过get参数?p=11这种方式使用page/11
// $page = empty($_GET['p'])?1:$_GET['p'];
// 获取数据
$offset = ($page-1)*$num;
$students = $this->model->select('student',['sno','sname','ssex'],['LIMIT'=>[$offset,$num]]);
// 获取总页数
$total = $this->model->count('student');
$pages = ceil($total/$num);
return $this->view->render('students/index',['page'=>$page,'students'=>$students,'pages'=>$pages]);
}
}
?>
我们在视图层 views/students/index.php 中也要修改对应的翻页按钮的href属性
举个例子 (?p=1)改为(/page/1)
<a href="<?=$page!=1?'/page/1':'javascript:;'?>">首页</a>
<a href="<?=$page!=1?'/page/'.$prev:'javascript:;'?>">上一页</a>
入口文件 public/index.php
<?php
// composer 自动加载器
require __DIR__.'\\..\\vendor\\autoload.php';
use app\controllers\StudentsController;
use app\models\StudentsModel;
use core\View;
$model = new StudentsModel;
$view= new View("../app/views");
$controller = new StudentsController($model,$view);
$params = [];
if(isset($_SERVER['REDIRECT_URL'])){
$res = array_values(array_filter(explode("/",$_SERVER['REDIRECT_URL'])));
// $res = ['0'=>page,'1'=>3]
$params[array_shift($res)] = array_shift($res);
// $params = ['page'=>3];
// var_dump($params);
}
$params['num'] = 5;
print_r(call_user_func_array([$controller,'index'],$params));
Apache的重写规则
启用 Apache 的 URL 重写功能, 需要开启mod_rewrite
模块. 然后在服务器配置文件或.htaccess
中修改服务配置:
AllowOverride all
Options FollowSysLinks
服务器配置文件和.htaccess
文件中都可以配置 URL 重写. 前者是服务器级别, 后者是目录级别.
更多重写规则:https://www.jianshu.com/p/6c103e426759
在 .htaccess 文件中配置重写规则
在.htaccess
文件中使用重写功能时, RewriteRule 负责匹配的 URI 是相对.htaccess
所在的目录而言的.
例如访问 http://example.com/subdir1/subdir2/subdir3:
- 如果
.htaccess
在网站根目录下, 那么RewriteRule
捕获的 URI 是subdir1/subdir2/subdir3
. - 如果
.htaccess
在 subdir1 目录下,RewriteRule
捕获的 URI 是subdir2/subdir3
.
RewriteRule
重写 URI 后的基准目录也是以.htaccess
所在的目录为准. 例如: 访问 http://example.com/foo
RewriteRule ^foo$ bar.php [L]
如果.htaccess
在根目录下, 重写后访问 http://example.com/bar.php. 如果在 subdir1 目录下, 重写后访问 http://example.com/subdir1/bar.php.
<IfModule mod_rewrite.c>
# 启用rewrite引擎
RewriteEngine On
# 重写规则: 匹配任意以htm后缀的文件, 将htm替换成php. ^(.*)\.htm$ 是一个正则表达式, 表示需要重写的部分, 此处指以任意字符开头, 以.htm结尾的部分. $1.php 是一个重写规则, $1 表示匹配到正则表达式中第一个子模式的字符串. [NC]: 表示重写规则如何应用, 该处表示不区分大小写. 整条规则即重写以任意字符开头, 以.htm结尾的部分, 重写为由匹配到的第一个子模式字符串和.php拼接成的字符串.
RewriteRule ^(.*)\.htm$ $1.php [NC]
</IfModule>
<IfModule mod_rewrite.c>
RewriteEngine On
# 设置目录级重写的基准URI
RewriteBase /subdir1/
RewriteRule ^(.*)\.htm$ $1.php [NC,L,R]
</IfModule>
RewriteBase
设置了重写的基准目录. 如果上例中.htaccess
位于网站根目录下, 访问的 http://example.com/foo.htm, 原本重写后的基准目录是网站根目录/
, 设置了RewriteBase
后变为/subdir1/
, 重写后实际访问 http://example.com/subdir1/foo.php.- 规则标志
L
: 表示如果可以匹配本条规则, 则不再继续往下匹配. - 规则标志
R
: 表示临时重定向, 即 302, 相当于[R=302]
.
<IfModule mod_rewrite.c>
Options +FollowSymlinks -Multiviews
RewriteEngine On
RewriteBase /
RewriteRule ^index.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>
隐藏入口文件,重写后地址为http://zhang.com实际访问的地址为http://zhang.com/index.php