Blogger Information
Blog 54
fans 6
comment 31
visits 107461
Popular Tutorials
More>
Latest Downloads
More>
Web Effects
Website Source Code
Website Materials
Front End Template
MVC实现和依赖注入、服务容器和门面的探讨
吾逍遥
Original
988 people have browsed it

一、MVC设计模式工作原理及简单实现

  • M: Model(模型层),最bottom一层,是核心数据层,程序需要操作的数据或信息.
  • V:View (视图层),最top一层,直接面向最终用户,视图层提供操作页面给用户,被誉为程序的外壳.
  • C:Controller(控制层),是middile层, 它负责根据用户从”视图层”输入的指令,选取”数据层”中的数据,然后对其进行相应的操作,产生最终结果。

mvc

其实日常开发中无论是仿站还是自己开发都基本按这个思路。如进行仿站一般都是从View开始,然后Model,最后想着Controller从Model数据库中读取数据展示到View中给客户浏览。而自己开发则是先考虑数据库Model的设计,怎么展示给客户View和如何Controller将数据库中数据传递给View。

下面就是简单的实现:

  1. //Model.php(模型)
  2. class Model{
  3. function getData(){
  4. $dsn="mysql:host=localhost;dbname=test;charset=UTF8";
  5. try{
  6. $pdo=new PDO($dsn,'root','root');
  7. $sql="SELECT * from user";
  8. return $pdo->query($sql)->fetchAll(PDO::FETCH_ASSOC);
  9. }catch(PDOException $e){
  10. echo '数据库连接错误:'.$e->getMessage();
  11. exit();
  12. }
  13. }
  14. }
  1. //View.php(视图)
  2. class View{
  3. function fetch(array $data){
  4. $html="<table border=1 cellspacing='0'>";
  5. $html.="<thead><tr bgColor='lightgray'><th>ID号</th><th>帐号</th><th>密码</th></tr></thead><tbody>";
  6. foreach($data as $user):
  7. $html.="<tr><td>{$user['id']}</td>";
  8. $html.="<td>{$user['uname']}</td>";
  9. $html.="<td>{$user['pwd']}</td></tr>";
  10. endforeach;
  11. $html.="</tbody></table>";
  12. return $html;
  13. }
  14. }
  1. //Controller.php(控制器)
  2. require_once 'Model.php';
  3. require_once 'View.php';
  4. class Controller
  5. {
  6. // 参数注入当前方法中,仅给当前方法使用
  7. function index(Model $model, View $view)
  8. {
  9. return $view->fetch($model->getData());
  10. }
  11. }
  12. // 测试输出代码
  13. $model=new Model();
  14. $view=new View();
  15. echo (new Controller())->index($model,$view);

二、依赖注入(参数注入)

在MVC模式中控制器Controller类中依赖模型类Model实例对象和视图类View实例对象function index(Model $model, View $view),此时我们可以在 当前成员方法中使用参数注入的方式,解决在当前类中使用其它类实例对象的问题,参数注入其实就是参数传递,它可以传递普通变量,也可是对象、闭包。

若是其它类的实例对象需要在当前类的众多成员方法中使用时( 共享或复用 ),若仍然采用将对象参数注入到成员方法中,将要多处写参数注入,此时建议方案就是能完 构造函数的参数注入 方式,将其它类的实例对象赋值给类的成员变量,这样当前类的所有成员方法都可访问,从而实现了其它类的共享或复用。

  1. // 类的共享或复用
  2. class Controller
  3. {
  4. private $model=null;
  5. private $view=null;
  6. // 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
  7. function __construct(Model $model, View $view)
  8. {
  9. $this->model=$model;
  10. $this->view=$view;
  11. }
  12. function index()
  13. {
  14. return $this->view->fetch($this->model->getData());
  15. }
  16. }
  17. $model=new Model();
  18. $view=new View();
  19. echo (new Controller($model,$view))->index();

补充: 以前看框架时,说优势之一就是使用依赖注入,后来经老师演示,原来就是参数传递,不得不说,好多文档说明故意提高了理解的门槛,看来理解基础再来学框架是最正确的学习路径。

三、服务容器(Container)

如果当前类依赖的对象较多,或者项目中有许多对象和类需要在项目中反复调用,可以将当前依赖的外部对象放到一个”服务容器”中进行统一管理。如TP6框架的Container类,服务容器最基本要有三个成员:对象容器(对象数组)、绑定对象方法和取对象方法。

  1. // 如果当前类依赖的对象较多,可以将当前依赖的外部对象放到一个"服务容器"中进行统一管理
  2. // 服务容器: 一个自动产生类/对象的工厂
  3. class Container
  4. {
  5. // 1、对象容器(对象数组)
  6. protected $instance = [];
  7. /**
  8. * 2、绑定对象:对象容器中添加对象
  9. * $abstract :类标识, 接口, 是外部对象在当前容器数组中的键名/别名 alias *
  10. * $concrete : 要绑定的类, 闭包或者实例, 传入一个对象或者一个闭包,前者需要我们先实例化对象才能往对象容器中添加, 闭包优势:我们使用这个对象的时候,才实例化对象
  11. */
  12. function bind($abstract, Closure $concrete)
  13. {
  14. $this->instance[$abstract] = $concrete;
  15. }
  16. //3.从对象容器中取出对象, 调用
  17. function make($abstract, $args = [])
  18. {
  19. return call_user_func_array($this->instance[$abstract], $args);
  20. }
  21. }

此时依赖注入就不是一个个类了,而是包含这些类的服务容器类Container,其实质仍然是引用类的实例对象,只不过这些实例对象通过bind方法保存到容器类中对象数组中,使用时使用make方法取出即可。

  1. // Controller2.php
  2. require_once 'Model.php';
  3. require_once 'View.php';
  4. require_once '../Container.php';
  5. // 类的共享或复用
  6. class Controller
  7. {
  8. private $model=null;
  9. private $view=null;
  10. // 参数注入构造函数中,赋值给类的成员变量,便于类的成员方法访问(共享或复用)
  11. function __construct(Container $container)
  12. {
  13. $this->model=$container->make('model');
  14. $this->view=$container->make('view');
  15. }
  16. function index()
  17. {
  18. return $this->view->fetch($this->model->getData());
  19. }
  20. }
  21. $container=new Container();
  22. $container->bind('model',function(){
  23. return new Model();
  24. });
  25. $container->bind('view',function(){
  26. return new View();
  27. });
  28. echo (new Controller($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的代码进行修改版,可以认清三者的查找成员时起点:

  1. class A {
  2. public static function foo() {
  3. static::who();
  4. }
  5. public static function who() {
  6. echo __CLASS__."<br>";
  7. }
  8. }
  9. class B extends A {
  10. public static function test() {
  11. // 指明具体类
  12. A::foo();
  13. // 调用static后期绑定静态成员
  14. parent::foo();
  15. self::foo();
  16. // parent和self区别
  17. parent::who();
  18. self::who();
  19. }
  20. public static function who() {
  21. echo __CLASS__."<br>";
  22. }
  23. }
  24. class C extends B {
  25. public static function who() {
  26. echo __CLASS__."<br>";
  27. }
  28. }
  29. C::test();

static

补充: 正如图片中所说,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。所以 “非转发调用的类名”是由指定类名开始的,由继承关系的类组成的队列。下面是官方代码的解析过程,可以具体理解下三者作用。

  1. * C::test(); //非转发调用 ,进入test()调用后,“上一次非转发调用”存储的类名为C
  2. *
  3. * //当前的“上一次非转发调用”存储的类名为C
  4. * public static function test() {
  5. * A::foo(); //非转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为A,然后实际执行代码A::foo(), 转 0-0
  6. * parent::foo(); //转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处的parent解析为A ,转1-0
  7. * self::foo(); //转发调用, 进入foo()调用后,“上一次非转发调用”存储的类名为C, 此处self解析为B, 转2-0
  8. * }
  9. *
  10. * 0-0
  11. * //当前的“上一次非转发调用”存储的类名为A
  12. * public static function foo() {
  13. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为A, 故实际执行代码A::who(),即static代表A,进入who()调用后,“上一次非转发调用”存储的类名依然为A,因此打印 “A”
  14. * }
  15. *
  16. * 1-0
  17. * //当前的“上一次非转发调用”存储的类名为C
  18. * public static function foo() {
  19. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
  20. * }
  21. *
  22. * 2-0
  23. * //当前的“上一次非转发调用”存储的类名为C
  24. * public static function foo() {
  25. * static::who(); //转发调用, 因为当前的“上一次非转发调用”存储的类名为C, 故实际执行代码C::who(),即static代表C,进入who()调用后,“上一次非转发调用”存储的类名依然为C,因此打印 “C”
  26. * }

4、后期静态绑定解决单例继承问题

  1. class A
  2. {
  3. protected static $_instance = null;
  4. static public function getInstance()
  5. {
  6. if (self::$_instance === null) {
  7. self::$_instance = new self();
  8. }
  9. return self::$_instance;
  10. }
  11. }
  12. class B extends A
  13. {
  14. }
  15. class C extends A{
  16. }
  17. $a = A::getInstance();
  18. $b = B::getInstance();
  19. $c = C::getInstance();
  20. var_dump($a);
  21. var_dump($b);
  22. var_dump($c);

static1

上面单例继承问题可通过static::解决,即getInstance()方法中self都替换成static就可以。在TP6框架的服务容器Container类中也是通过static::解决单例继承问题的。

  1. //TP6的Container.php中实例方法
  2. public static function getInstance()
  3. {
  4. if (is_null(static::$instance)) {
  5. static::$instance = new static;
  6. }
  7. if (static::$instance instanceof Closure) {
  8. return (static::$instance)();
  9. }
  10. return static::$instance;
  11. }

五、Facade(门面)

门面类Facade其实相当于中间层,先对想静态调用的类定义继承于门面类的子类,然后在该子类中定义相应的静态方法,实质是再调用对应类的非静态方法。为了相同类名,我参考了TP6框架的做法,将门面类统一命名空间为Facade。下面演示是基于服务容器类的门面类,单独类需要门面类似定义即可。

  1. // Facade.php
  2. namespace Facade;
  3. require_once 'Container.php';
  4. // 门面类
  5. class Facade
  6. {
  7. protected static $container = null;
  8. static function instance(\Container $container)
  9. {
  10. static::$container = $container;
  11. }
  12. }
  13. // 需要静态调用的类继承门面类,为了统一,和原类名相同
  14. class Model extends Facade
  15. {
  16. static function getData()
  17. {
  18. return static::$container->make('model')->getData();
  19. }
  20. }
  21. class View extends Facade
  22. {
  23. static function fetch($data)
  24. {
  25. return static::$container->make('view')->fetch($data);
  26. }
  27. }

上面已经定义了基于服务容器类Container的门面类,将服务容器的对象数组成员赋值给Facade的静态成员,从而被所有继承Facade类的子类共享。

  1. // Controller3.php
  2. require_once 'Facade.php';
  3. require_once 'Model.php';
  4. require_once 'View.php';
  5. use Facade\Facade;
  6. use Facade\Model;
  7. use Facade\View;
  8. // 类的共享或复用
  9. class Controller
  10. {
  11. function __construct(Container $container)
  12. {
  13. Facade::instance($container);
  14. }
  15. function index()
  16. {
  17. return View::fetch(Model::getData());
  18. }
  19. }
  20. $container=new Container();
  21. $container->bind('model',function(){
  22. return new \Model();
  23. });
  24. $container->bind('view',function(){
  25. return new \View();
  26. });
  27. echo (new Controller($container))->index();

补充: 在我的Githubhttps://github.com/woxiaoyao81/phpcn13或Giteehttps://gitee.com/freegroup81/phpcn13可以下载到参考TP6运行模式的《PHP从零开始写MVC》压缩包源码,我是在从TP官网论坛下载到的,可以作为本博文的进阶练习。

Correcting teacher:灭绝师太灭绝师太

Correction status:qualified

Teacher's comments:
Statement of this Website
The copyright of this blog article belongs to the blogger. Please specify the address when reprinting! If there is any infringement or violation of the law, please contact admin@php.cn Report processing!
All comments Speak rationally on civilized internet, please comply with News Comment Service Agreement
2 comments
吾逍遥 2020-12-15 16:25:28
声明类关注点是范围解析符::后面的方法,而调用类关注点是::前面调用类,理解了
2 floor
灭绝师太 2020-12-15 15:27:54
声明类和调用类很好区分, 声明类就是指一个静态方法的声明类, 如果当前类被继承, 使用子类去访问该静态方法, 那么子类就是该静态方法的调用类.
1 floor
Author's latest blog post