一、MVC设计模式工作原理及简单实现M: Model(模型层),最bottom一层,是核心数据层,程序需要操作的数据或信息.
V:View (视图层),最top一层,直接面向最终用户,视图层提供操作页面给用户,被誉为程序的外壳.
C:Controller(控制层),是middile层, 它负责根据用户从”视图层”输入的指令,选取”数据层”中的数据,然后对其进行相应的操作,产生最终结果。
其实日常开发中无论是仿站还是自己开发都基本按这个思路。如进行仿站一般都是从View开始,然后Model,最后想着Controller从Model数据库中读取数据展示到View中给客户浏览。而自己开发则是先考虑数据库Model的设计,怎么展示给客户View和如何Controller将数据库中数据传递给View。
下面就是简单的实现:
//Model.php(模型)
classModel{
functiongetData(){
$dsn="mysql:host=localhost;dbname=test;charset=UTF8";
try{
$pdo=newPDO($dsn,'root','root');
$sql="SELECT * from user";
return$pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
}catch(PDOException$e){
echo'数据库连接错误:'.$e->getMessage();
exit();
}
}
}
//View.php(视图)
classView{
functionfetch(array $data){
$html="
$html.="ID号帐号密码
";foreach($dataas$user):
$html.="
{$user['id']}";$html.="
{$user['uname']}";$html.="
{$user['pwd']}";endforeach;
$html.="
";return$html;
}
}
//Controller.php(控制器)
require_once'Model.php';
require_once'View.php';
classController
{
// 参数注入当前方法中,仅给当前方法使用
functionindex(Model$model,View$view)
{
return$view->fetch($model->getData());
}
}
// 测试输出代码
$model=newModel();
$view=newView();
echo(newController())->index($model,$view);
二、依赖注入(参数注入)
在MVC模式中控制器Controller类中依赖模型类Model实例对象和视图类View实例对象function index(Model $model, View $view),此时我们可以在 当前成员方法中使用参数注入的方式,解决在当前类中使用其它类实例对象的问题,参数注入其实就是参数传递,它可以传递普通变量,也可是对象、闭包。
若是其它类的实例对象需要在当前类的众多成员方法中使用时( 共享或复用 ),若仍然采用将对象参数注入到成员方法中,将要多处写参数注入,此时建议方案就是能完 构造函数的参数注入 方式,将其它类的实例对象赋值给类的成员变量,这样当前类的所有成员方法都可访问,从而实现了其它类的共享或复用。
// 类的共享或复用
classController
{
private$model=null;
private$view=null;
// 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
function__construct(Model$model,View$view)
{
$this->model=$model;
$this->view=$view;
}
functionindex()
{
return$this->view->fetch($this->model->getData());
}
}
$model=newModel();
$view=newView();
echo(newController($model,$view))->index();
补充: 以前看框架时,说优势之一就是使用依赖注入,后来经老师演示,原来就是参数传递,不得不说,好多文档说明故意提高了理解的门槛,看来理解基础再来学框架是最正确的学习路径。
三、服务容器(Container)
如果当前类依赖的对象较多,或者项目中有许多对象和类需要在项目中反复调用,可以将当前依赖的外部对象放到一个”服务容器”中进行统一管理。如TP6框架的Container类,服务容器最基本要有三个成员:对象容器(对象数组)、绑定对象方法和取对象方法。
// 如果当前类依赖的对象较多,可以将当前依赖的外部对象放到一个"服务容器"中进行统一管理
// 服务容器: 一个自动产生类/对象的工厂
classContainer
{
// 1、对象容器(对象数组)
protected$instance=[];
/**
* 2、绑定对象:对象容器中添加对象
* $abstract :类标识, 接口, 是外部对象在当前容器数组中的键名/别名 alias *
* $concrete : 要绑定的类, 闭包或者实例, 传入一个对象或者一个闭包,前者需要我们先实例化对象才能往对象容器中添加, 闭包优势:我们使用这个对象的时候,才实例化对象
*/
functionbind($abstract,Closure$concrete)
{
$this->instance[$abstract]=$concrete;
}
//3.从对象容器中取出对象, 调用
functionmake($abstract,$args=[])
{
returncall_user_func_array($this->instance[$abstract],$args);
}
}
此时依赖注入就不是一个个类了,而是包含这些类的服务容器类Container,其实质仍然是引用类的实例对象,只不过这些实例对象通过bind方法保存到容器类中对象数组中,使用时使用make方法取出即可。
// Controller2.php
require_once'Model.php';
require_once'View.php';
require_once'../Container.php';
// 类的共享或复用
classController
{
private$model=null;
private$view=null;
// 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
function__construct(Container$container)
{
$this->model=$container->make('model');
$this->view=$container->make('view');
}
functionindex()
{
return$this->view->fetch($this->model->getData());
}
}
$container=newContainer();
$container->bind('model',function(){
returnnewModel();
});
$container->bind('view',function(){
returnnewView();
});
echo(newController($container))->index();
四、self、parent和static探讨
本来要讲Facade(门面)技术的,但它静态实现依赖static的后期静态绑定,自PHP 5.3.0起,PHP增加了一个叫做后期静态绑定的功能,用于在继承范围内引用静态调用的类。在群里也咨询老师了,老师的解释是后期静态绑定,是在继承上下文中,self只能和声明类进行绑定,并不能和调用类进行绑定。,同时也查阅了PHP官方的解释,最终梳理成以下几个理解点:
1、self、parent和static到底代表谁?
首先三者都是相当于魔术变量,在实际编译时会指向具体的类,都不能在类外调用,只能在类中使用。至于老师说的声明类和调用类,如何区分声明类和调用类就不好界定。通过代码测试,我的发现它们区别就是 查找类中成员范围起点不同 。
self:从当前类中开始查找成员,若有(重写时)则结束查找,没有(未重写时)则依次向父类查找成员,直到找到为止。如下面代码中self::who();就是先从B类中查找成员,刚好有就输出B
parent:直接跳转当前类中成员,即使重写也忽略,从父类开始查找成员,后面查找同self,唯一不同就是起点。如下面代码中parent::who();虽然B类已经定义who方法,但它直接从父类A开始查找,找到who方法,输出为A。
static:若是static访问成员,则是从明确指定类名开始查找方法。如下面代码中parent::foo();self::foo();按parent和self规则其实最终找到就是A类中foo方法,但它是static访问成员,所以它要用明确指明类来替换,而下面代码是C::test()导致这种查找,因为C类中没有tes方法,它从父类B中找到,此时明确调用类是C不是B,这点最容易误导。
下面是基于PHP官方区分self、parent和static的代码进行修改版,可以认清三者的查找成员时起点:
classA{
publicstaticfunctionfoo(){
static::who();
}
publicstaticfunctionwho(){
echo __CLASS__."
";
}
}
classBextendsA{
publicstaticfunctiontest(){
// 指明具体类
A::foo();
// 调用static后期绑定静态成员
parent::foo();
self::foo();
// parent和self区别
parent::who();
self::who();
}
publicstaticfunctionwho(){
echo __CLASS__."
";
}
}
classCextendsB{
publicstaticfunctionwho(){
echo __CLASS__."
";
}
}
C::test();
补充: 正如图片中所说,static也同self和parent,只是指明 查找成员起点不同 而已。若上面代码中C类没有重写who方法,即注释掉C类中who方法,此时由于C类没有who方法,C类调用父类B中who方法,此时输出是:A B B A B。
2、转发调用与非转发调用转发调用:指的是通过以下几种方式进行的静态调用 :self::,parent::,static::以及 forward_static_call()。
非转发调用:明确指定类名的静态调用 。例如Foo::foo();
上面是某网文对两种调用的解释,其中第一个是官方的解释,但我认为它们只是表面认识。经测试 两种调用都是针对是否重写继承父类的成员 来说的。若是重写了父类的成员,则明确指定类名或self::访问都直接访问当前类中成员,终止了向上查找,是非转发调用。而没有重写父类的成员则无论是指定类名,还是上面几种都是转发调用,上面代码中C::test()虽然指定类名了,但它转发调用B类中test方法。
3、后期静态绑定
官方解释说:存储了在上一个“非转发调用”(non-forwarding call)的类名。其实我觉得关键词是两个”非转发调用”和”类名”,上面代码中C::test()输出static::who();是从C类开始查找who成员方法,结果找到了,所以此时”非转发调用的类名”是C,若是C类中没有重写who方法(即注释掉C类的who方法),在B类中找到who成员方法,此时”非转发调用的类名”是B。所以 “非转发调用的类名”是由指定类名开始的,由继承关系的类组成的队列。下面是官方代码的解析过程,可以具体理解下三者作用。
*C::test();//非转发调用 ,进入test()调用后,“上一次非转发调用”存储的类名为C
*
*//当前的“上一次非转发调用”存储的类名为C
*publicstaticfunctiontest(){
*A::foo();//非转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为A,然后实际执行代码A::foo(), 转 0-0
*parent::foo();//转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处的parent解析为A ,转1-0
*self::foo();//转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处self解析为B, 转2-0
*}
*
*0-0
*//当前的“上一次非转发调用”存储的类名为A
*publicstaticfunctionfoo(){
*static::who();//转发调用, 因为当前的“上一次非转发调用”存储的类名为A, 故实际执行代码A::who(),即static代表A,进入who()调用后,“上一次非转发调用”存储的类名依然为A,因此打印 “A”
*}
*
*1-0
*//当前的“上一次非转发调用”存储的类名为C
*publicstaticfunctionfoo(){
*static::who();//转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
*}
*
*2-0
*//当前的“上一次非转发调用”存储的类名为C
*publicstaticfunctionfoo(){
*static::who();//转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
*}
4、后期静态绑定解决单例继承问题classA
{
protectedstatic$_instance=null;
staticpublicfunctiongetInstance()
{
if(self::$_instance===null){
self::$_instance=newself();
}
returnself::$_instance;
}
}
classBextendsA
{
}
classCextendsA{
}
$a=A::getInstance();
$b=B::getInstance();
$c=C::getInstance();
var_dump($a);
var_dump($b);
var_dump($c);
上面单例继承问题可通过static::解决,即getInstance()方法中self都替换成static就可以。在TP6框架的服务容器Container类中也是通过static::解决单例继承问题的。
//TP6的Container.php中实例方法
publicstaticfunctiongetInstance()
{
if(is_null(static::$instance)){
static::$instance=newstatic;
}
if(static::$instanceinstanceofClosure){
return(static::$instance)();
}
returnstatic::$instance;
}
五、Facade(门面)
门面类Facade其实相当于中间层,先对想静态调用的类定义继承于门面类的子类,然后在该子类中定义相应的静态方法,实质是再调用对应类的非静态方法。为了相同类名,我参考了TP6框架的做法,将门面类统一命名空间为Facade。下面演示是基于服务容器类的门面类,单独类需要门面类似定义即可。
// Facade.php
namespaceFacade;
require_once'Container.php';
// 门面类
classFacade
{
protectedstatic$container=null;
staticfunctioninstance(\Container $container)
{
static::$container=$container;
}
}
// 需要静态调用的类继承门面类,为了统一,和原类名相同
classModelextendsFacade
{
staticfunctiongetData()
{
returnstatic::$container->make('model')->getData();
}
}
classViewextendsFacade
{
staticfunctionfetch($data)
{
returnstatic::$container->make('view')->fetch($data);
}
}
上面已经定义了基于服务容器类Container的门面类,将服务容器的对象数组成员赋值给Facade的静态成员,从而被所有继承Facade类的子类共享。
// Controller3.php
require_once'Facade.php';
require_once'Model.php';
require_once'View.php';
useFacade\Facade;
useFacade\Model;
useFacade\View;
// 类的共享或复用
classController
{
function__construct(Container$container)
{
Facade::instance($container);
}
functionindex()
{
returnView::fetch(Model::getData());
}
}
$container=newContainer();
$container->bind('model',function(){
returnnew\Model();
});
$container->bind('view',function(){
returnnew\View();
});
echo(newController($container))->index();
补充: 在我的Githubhttps://github.com/woxiaoyao81/phpcn13或Giteehttps://gitee.com/freegroup81/phpcn13可以下载到参考TP6运行模式的《PHP从零开始写MVC》压缩包源码,我是在从TP官网论坛下载到的,可以作为本博文的进阶练习。