Home  >  Article  >  Backend Development  >  Build your own PHP MVC framework

Build your own PHP MVC framework

不言
不言Original
2018-06-07 11:42:585304browse

This article mainly introduces the method of building your own PHP MVC framework, and analyzes in detail the specific steps, related operating techniques and precautions for building the MVC framework in PHP. Friends in need can refer to the following

Details of this article Describes how to build your own PHP MVC framework. Share it with everyone for your reference, the details are as follows:

Preface

When it comes to writing the MVC framework for PHP, the first word that comes to mind -- "Making the wheel", yes, the PHP framework written by a programmer who does not have deep skills is definitely not as good as those frameworks that are written by masters and have been tested by time and various projects. But I still prepared and did it, mainly because:

I think I understand all aspects of PHP, but I have only learned PHP for a short time, and the foundation is not solid. There are many parameters of commonly used functions. I occasionally consult the manual, and I have only briefly looked at some of PHP's newer features such as namespaces and reflection, but have not actually applied them.

The knowledge of PHP is rich and complex. An ordinary project is often dominated by business logic code, and a framework is a project that can integrate these knowledge points.

When I write a framework, I will also refer to the source code of some frameworks I have used, such as TP/CI/YII, etc., which can also help me understand the framework when I look at the source code. , making it easier to accept the framework you want to use in the future.

So, the purpose of making wheels this time is not to make wheels but to become familiar with the process of making wheels, summarize the characteristics of wheels, and use wheels better.

If you want to write a complete PHP framework, you need to master a lot of PHP knowledge points, such as design patterns, iterators, events and hooks, etc., as well as the flexible application of many basic knowledge. I think I can't fully control these, so my step is to build a skeleton myself first, and then refer to the characteristics of different PHP frameworks to slowly improve it. Due to work reasons and having to catch up on programming basics such as algorithms and networks at night, I may only have time to update the PHP framework part on weekends. After updating the framework functions, I will summarize the knowledge points used and update the blog post.

First put the current source code of the framework: GITHUB/zhenbianshu

Overall framework

First of all, summarize the workflow of PHP's MVC framework:

Simply put, it uses an entry file to accept requests, select routes, process requests, and return results.

Of course, what I have summarized in a few sentences actually requires a lot of work. The PHP framework will define constants, load configuration files and basic classes every time it accepts a request, and make logical judgments based on the accessed URL. , select the corresponding (module) controller and method, and automatically load the corresponding class. After processing the request, the framework will select and render the corresponding template file and return the response in the form of an HTML page. When processing logic, error and exception handling should also be considered.

1. As an MVC framework, there must be a unique entry file to control the overall situation. All access requests will first enter this entry file, such as index.php in the root directory of my framework. In it, I define The base folder path, the current environment, and the level of error reporting are defined based on the current environment.

2. To load other files in PHP, use require and include. They load the contents of the target file into the current file and replace the require or include statement. require is executed after loading, while include is They are loaded and executed when needed, and their _once structures indicate that they will only be executed once when written multiple times.

3. The configuration variables in the framework are saved in a dedicated configuration file. Here I imitated the array return method in TP and used a compileConf() function to parse the array. Define the keys of the array as constants and the values ​​as the values ​​of the array.

if (!function_exists('compile_conf')) {
  function compileConf($conf) {
    foreach ($conf as $key => $val) {
    if(is_array($val)){
       compileConf($val);
      }else{
      define($key, $val);
      }
    }
   }
}
compileConf(require_once CONF_PATH.'config.php');

Namespace and automatic loading

Why put namespace and automatic loading Let’s talk about it together?

In a PHP project, when there are many classes, it will cause confusion if the class names are repeated, and files with the same name cannot exist in the same folder, so at this time the namespace and the folder are paired. Appeared. Folders are boxes one by one. In my understanding, namespace is like a label, and boxes correspond to labels. When we define classes, we put various classes in different boxes and attach corresponding labels. When automatically loading a class, we can easily find the corresponding box (folder) based on the label (namespace) and then find the corresponding class file.

As for the automatic loading of classes, we know the __autoload() magic function. It will be automatically called when you instantiate an object that cannot be found in the current path. According to the passed in class name, in the function body Load the corresponding class file.

现在我们多用spl_autoload_register()函数,它可以注册多个函数来代替__autoload函数的功能,我们传入一个函数名为参数,spl_autoload_register会将这个函数压入栈中,在实例化一个当前路径内找不到的类时,系统将会将函数出栈依次调用,直到实例化成功。

spl_autoload_register('Sqier\Loader::autoLoad');
class Loader {
public static function autoLoad($class) {
  //如果有的话,去除类最左侧的\
  $class = ltrim($class, '\\');
  //获取类的路径全名
  $class_path = str_replace('\\', '/', $class) . EXT;
  if (file_exists(SYS_PATH . $class_path)) {
    include SYS_PATH . $class_path;
    return;
  }
  if (file_exists(APP_PATH . $class_path)) {
    include APP_PATH . $class_path;
    return;
  }
}

现在Loader类还是一个简单的类,待以后慢慢完善。

路由选择

接下来就是路由选择了,其本质是根据当前定义的全局URL模式选择合适的方法来分析传入的URI,加载对应的类,并实现对应的方法。

class Router {
public static $uri;
public static function bootstrap() {
  self::$uri = $_SERVER['REQUEST_URI'];
  switch (URL_MODE) {
    case 1: {
      self::rBoot();
      break;
    }
    default: {
      self::rBoot();
    }
  }
}
public static function rBoot() {
  $router = isset($_GET['r']) ? explode('/', $_GET['r']) : [
    'index',
    'index'
  ];
  $cName = 'Controller\\' . ucfirst($router[0]);
  $aName = isset($router[1]) ? strtolower($router[1]) . 'Action' : 'indexAction';
  $controller = new $cName();
  $controller->$aName();
  }
}

这样,我在地址栏输入 zbs.com/index.php?r=index/login 后,系统会自动调用/app/Controller/Index.php下的login方法。完成了这么一个简单的路由。

阶段总结:

接下来我会优化现有的工具类,添加显示层,添加数据库类,还会将一些别的框架里非常cool的功能移植进来~

接上文(代码有所更新),继续完善框架(二):

对于本次更新,我想说:

① 本框架由本人挑时间完善,而我还不是PHP大神级的人物,所以框架漏洞难免,求大神们指出。
② 本框架的知识点应用都会写在博客里,大家有什么异议的可以一起讨论,也希望看博客的也能学习到它们。
③ 本次更新,更新了函数规范上的一些问题,如将函数尽量的独立化,每一个函数尽量只单独做好一件事情,尽量减少函数依赖。还对框架的整体优化了一下,添加了SQ全局类,用以处理全局函数,变量。

回调函数

替换了很low的类名拼装实例化,然后拼装方法名的用法,使用PHP的回调函数方式:

原代码:

$controller_name = 'Controller\\' . self::$c_name;
$action_name = self::$a_name . 'Action';
$controller = new $controller_name();
$controller->$action_name();

修改后代码

$controller_name = 'Controller\\' . self::$c_name;
$controller = new $controller_name();
call_user_func([
  $controller,
  self::$a_name . 'Action'
]);

这里介绍一下PHP的函数回调应用方式:call_user_func和call_user_func_array:

call_user_func ( callback $function [, mixed $parameter [, mixed $... ]] )

调用第一个参数所提供的用户自定义的函数。

返回值:返回调用函数的结果,或FALSE。

call_user_func_array()的用法跟call_user_func类似,只不过传入的参数params整体为一个数组。

另外,call_user_func系列函数还可以传入在第一个参数里传入匿名参数,可以很方便的回调某些事件,这些特性在复杂的框架里应用也十分广泛,如yii2的事件机制里回调函数的使用就是基于此。

VIEW层和ob函数

框架在controller的基类中定义了render方法来渲染页面,它会调用类VIEW的静态函数来分析加载对应页面的模板。

public static function display($data, $view_file) {
  if(is_array($data)) {
    extract($data);//extract函数解析$data数组中的变量
  }else {
    //抛出变量类型异常
  }
  ob_start();
  ob_implicit_flush(0);
  include self::checkTemplate($view_file);//自定义checkTemplate函数,分析检查对应的函数模板,正常返回路径
  $content = ob_get_clean();
  echo $content;
}

这里重点说一下ob(output buffering)系列函数,其作用引用简明代魔法的ob作用介绍:

① 防止在浏览器有输出之后再使用setcookie,或者header,session_start函数造成的错误。其实这样的用法少用为好,养成良好的代码习惯。
② 捕捉对一些不可获取的函数的输出,比如phpinfo会输出一大堆的HTML,但是我们无法用一个变量例如$info=phpinfo();来捕捉,这时候ob就管用了。
③ 对输出的内容进行处理,例如进行gzip压缩,例如进行简繁转换,例如进行一些字符串替换。
④ 生成静态文件,其实就是捕捉整页的输出,然后存成文件,经常在生成HTML,或者整页缓存中使用。

它在ob_start()函数执行后,打开缓冲区,将后面的输出内容装进系统的缓冲区,ob_implicit_flush(0)函数来关闭绝对刷送(echo等),最后使用ob_get_clean()函数将缓冲区的内容取出来。

类__URL__常量和全局类

TP里的__URL__等全局常量用着很方便,可以很简单的实现跳转等操作,而定义它的函数createUrl函数我又想重用,于是借鉴YII的全局类定义方法:

定义基类及详细方法(以后的全局方法会写在这里)

class BaseSqier{
  //方法根据传入的$info信息,和当前URL_MODE解析返回URL字符串
  public static function createUrl($info = '') {
    $url_info = explode('/', strtolower($info));
    $controller = isset($url_info[1]) ? $url_info[0] : strtolower(CONTROLLER);
    $action = isset($url_info[1]) ? $url_info[1] : $url_info[0];
    switch(URL_MODE){
      case URL_COMMON:
        return "/index.php?r=" . $controller . '/' . $action;
      case URL_REWRITE:
        return '/' .$controller . '/' . $action;
    }
  }
}

在启动文件中定义类并继承基类;

require_once SQ_PATH.'BaseSqier.php';
class SQ extends BaseSqier{
}

在全局内都可以直接使用SQ::createUrl()方法来创建URL了。这样,定义__URL__常量就很轻松了。

用单例模式定义数据库连接基类

class Db {
  protected static $_instance;
  public static function getInstance() {
    if(!(self::$_instance instanceof self)) {
      self::$_instance = new self();
    }
    return self::$_instance;
  }
  private function __construct() {
    $link = new \mysqli(DB_HOST, DB_USER, DB_PWD, DB_NAME) or die("连接数据库失败,请检查数据库配置信息!");
    $link->query('set names utf8');
  }
  public function __clone() {
    return self::getInstance();
  }
}

使用单例模式的核心是:

① 私有化构造函数,使无法用new来创建对象,也防止子类继承它并改写其构造函数;
② 用静态变量存放当前对象,定义静态方法来返回对象,如对象还未实例化,实例化一个,存入静态变量并返回。
③ 构造其__clone魔术方法,防止clone出一个新的对象;

DB类的sql查询函数

DB查询函数是一个很复杂的部分,它是一个自成体系的东西,像TP和YII的查询方法都有其独特的地方。我这里暂时先借用TP的MODEL基类,有时间再慢慢补这个。

嗯,介绍一下像TP的查询里的方法联查的实现,其诀窍在于,在每个联查方法的最后都用 return this 来返回已处理过的查询对象。

阶段总结:

yii2里的数据表和model类属性之间的映射很酷(虽然被深坑过), 前面一直避开的模块(module,我可以想像得到把它也添加到URI时解析的麻烦)有时间考虑一下。

接上文,继续完善框架(三)

本次更新的主要内容有:

① 介绍了异常处理机制
② 完善了异常和错误处理
③ 数据表跟Model类的映射

异常处理

异常处理:异常处理是编程语言或计算机硬件里的一种机制,用于处理软件或信息系统中出现的异常状况(即超出程序正常执行流程的某些特殊条件)

异常处理用于处理程序中的异常状况,虽说是“异常状态”,但仍然还是在程序编写人员的预料之中,其实程序的异常处理完全可以用‘if else'语句来代替,但异常处理自然有其优势之处。

个人总结其优点如下:

① 可以快速终止流程,重置系统状态,清理变量和内存占用,在普通WEB应用中,一次请求结束后,FAST CGI会自动清理变量和上下文,但如果在PHP的命令行模式执行守护脚本时,它的效果就会很方便了。

② 大量的if else语句会使代码变得繁杂难懂,使用异常处理可以使程序逻辑更清晰易懂,毕竟处理异常的入口只有catch语句一处。

③ 一量程序中的函数出现异常结果或状况,如果使用函数的return方式返回异常信息,层层向上,每一次都要进行return判断。使用异常处理我们可以假设所有的返回信息都是正常的,避免了大量的代码重复。

虽然将代码放在try catch块中会有微微的效率差,但是跟这些优点一比,这点消耗就不算什么了。那么PHP的异常处理怎么使用呢?

PHP内置有Exception类,使得我们可以通过实例化异常类来抛出异常。我们将代码放在try语句中执行,并在其后用catch试图捕捉到在try代码块中抛出的异常,并对异常进行处理。我们还可以在catch代码段后使用finally语句块,无论是否有异常都会执行finally代码块的代码,try catch语句形如下面代码:

try{
  throw new Exeption('msg'[,'code',$previous_exeception]);
}catch(Exeption $var) {
  process($var);
}catch(MyException $e){
  process($e)
}finally{
  dosomething();
}

使用try catch语句,需要注意:

① 当我们抛出异常时,会实例化一个异常类,此异常类可以自己定义,但在catch语句中,我们需要规定要捕获的异常对象的类名,并且只能捕获到特定类的异常对象,当然我们可以在最后捕获一个异常基类(PHP内置异常类)来确保异常一定能被捕获。

② 在抛出异常时,程序会被终止,并回溯代码找到第一个能捕获到它的catch语句,try catch语句是可以嵌套的,并且如上面代码所示 cacth语句是可以多次定义的。

finally块会在try catch块结束后执行,即使在try catch块中使用return返回,程序没有执行到最后。

框架里的异常处理

说了那么多异常相关(当然解释这些也是为了能理解和使用框架),那么框架里要怎么实现呢?

重写异常类

我们可以重写异常类,完善其内部方法:

<?php
class Exception
{
  protected $message = &#39;Unknown exception&#39;;  // 异常信息
  protected $code = 0;            // 异常代码
  protected $file;              // 发生异常的文件名
  protected $line;              // 发生异常的代码行号
  function __construct($message = null, $code = null,$previous_exeception = null);
  final function getMessage();        // 返回异常信息
  final function getCode();          // 返回异常代码
  final function getFile();          // 返回发生异常的文件名
  final function getLine();          // 返回发生异常的代码行号
  final function getTrace();         // 返回异常trace数组
  final function getTraceAsString();     // 返回异常trace信息
  /**
   * 记录错误日志
   */
  protected function log(){
    Logger::debug();
  }
}

如上,final方法是不可以重写的,除此之外,我们可以定义自己的方法,如记录异常日志,像我自定义的log方法,在catch代码块中,就可以直接使用$e->log来记录一个异常日志了。

注册全局异常方法

我们可以使用set_exception_handler('exceptionHandler')来全局捕获没有被catch块捕获到的异常,此异常处理函数需要传入一个异常处理对象,这样可以分析此异常处理信息,避免系统出现不人性化的提示,增强框架的健壮性。

function exceptionHandler($e) {
  echo &#39;有未被捕获的异常,在&#39; . $e->getFile() . "的" . $e->getLine() . "行!";
}

其他全局函数

顺便再说一下其他的全局处理函数:

set_shutdown_function('shutDownHandler')来执行脚本结束时的函数,此函数即使是在ERROR结束后,也会自动调用。

set_error_handler('errorHandler')在PHP发生错误时自动调用,注意,必须在已注册错误函数后才发出的错误才会调用。函数参数形式应为($errno, $errstr, $errfile, $errline);

但是要注意这些全局函数需要在代码段的前面已经定义过再注册。

数据表和Model类的ActiveRecord映射

初次使用yii2的ActivceRecord类觉得好方便,只需要定义其字段同名属性再调用save方法就OK了(好神奇啊),它是怎么实现的呢,看了下源码,明白了其大致实现过程(基类)。

1. 使用‘describe table_name' 查询语句;
2. 分析查询结果:对每一个字段,有Field(字段名)、Type(数据类型)、Null(是否为空)、Key(索引信息,‘PRI'表示为主键)、Default(默认值)、Extra(附加信息,如auto_increment)
3. 通过判断其主键($row['KEY'] == 'PRI')信息,保存时看是否有主键信息,若存在,则为更新;不存在,则插入。
4. 另外,解析出来的字段信息还有更多妙用~~

相关推荐:

thinkPHP框架实现图像裁剪、缩放、加水印的方法

The above is the detailed content of Build your own PHP MVC framework. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn