2015年8月

   想了解PHP-MVC框架的原理和实现方式是什么?在这里,我们用PHP开发一个自己的MVC框架。小巧精悍。深入理解市面上PHP主流的MVC框架原理。唯一入口文件该干什么,如何自动载入。本章代码基于PHP5.3+,采用命名空间。

    PHP框架是什么?http://baike.baidu.com/link?url=3IBWTY1jzqQiHAfVWRn6BzXvkUxkWTDffhe7_J4o7Eqs2Hxp06PMnAXsK82vGdnQEfEYuueszQ3_EFdxMUOvEa

    MVC是什么?http://baike.baidu.com/view/5432454.htm?fromtitle=mvc&fromid=85990&type=syn

    源码地址:https://github.com/lixuancn/LaneSmartFW

    开源协议:Do What The Fuck You Want To Public License


一、起名:

    先给我们的PHP-MVC框架起个名字,叫宇宙无敌框架UniverseInvincibleFrameWork


二、实现功能

    1、MVC分层

    2、唯一入口

    3、关键常量可配置

    4、自动载入函数

    5、路由分发

    6、数据库工厂

    7、多数据支持

    8、多项目支持


三、详细分解如何PHP-MVC框架

    1、MVC分层

        1)、目录结构

框架根目录



项目一的目录



配置文件目录



框架核心文件目录



框架核心文件之数据库目录



        2)、目录简介

            (1)、Home、Admin是项目名,可以无限扩展

            (2)、Config是配置文件所在目录,UniverseInvincibleFrameWork是框架核心文件所在目录

            (3)、Index.php是唯一入口文件

            (4)、Home目录下就是标准的Controller、Model、View,另外新增了Service

            (5)、UniverseInvincibleFrameWork目录下是核心框架入口类、自动载入类、路由类已经数据库文件所在的DB目录

            (6)、DB目录是数据库相关操作。比如数据库工厂类,接口规范类,CURD操作等。


    2、唯一入口

        1)、采用单一入口模式进行项目部署和访问,无论完成什么功能,一个项目都有一个统一的入口。

        2)、只需要引入框架核心文件App.php,然后执行该类的方法

<?php

/**

 * 宇宙无敌框架UniverseInvincibleFrameWork

 * 唯一入口

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:17

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

//引入框架核心文件

require_once 'UniverseInvincibleFrameWork/App.php';

//初始化框架

$obj = new UniverseInvincibleFrameWork\App();

$obj->init();

        3)、框架核心文件源码:

<?php

namespace UniverseInvincibleFrameWork;



class App{

    public function init(){

        //设置头 - utf-8

        $this->_setHeader();

        //载入系统配置文件

        $this->_loadSysFile();

        //自动载入函数

        $this->_setAutoload();

        //设置路由

        $this->_setRoute();

    }


    /**

     * 载入系统配置文件

     */

    private function _loadSysFile(){

        require_once dirname(__FILE__).'/Function.php';

        //1、 require_once dirname(__FILE__).'/../config/config.php';

        //2、$GLOBALS['config'] = config.php的所有内容

        $GLOBALS['config'] = require_once dirname(__FILE__).'/../config/config.php';

    }


    /**

     * 头

     */

    private function _setHeader(){

        header('Content-type: text/html; charset=UTF-8');

    }


    /**

     * 自动载入函数

     */

    private function _setAutoload(){

        //自动载入函数

        require_once dirname(__FILE__).'/../UniverseInvincibleFrameWork/Autoload.php';

        $autoload = new Autoload();

        $autoload->register();

    }


    /**

     * 设置路由

     */

    private function _setRoute(){

        $routeObj = new Route();

        $routeObj->parse();

    }

}

    3、关键常量可配置

        1)、谁也不会傻呼呼的到把数据库链接信息等配置信息写死到代码里,那么就必须有一个配置文件。它定义系统常量,包括但不限于项目名称、数据库账号密码,默认应用名称/控制器/方法名等

        2)、配置文件还有个好处,定义生长环境、测试环境、开发环境等不同的参数,可以根据来访域名、所在机器IP等信息来使自动选择加载不同的系统和数据库配置。

<?php

/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:28

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

return array(

    //默认加载的项目

    'DEFAULT_APP_NAME' => 'Home',

    //默认加载的控制器

    'DEFAULT_CONTROLLER' => 'Index',

    //默认加载的方法

    'DEFAULT_METHOD' => 'index',

 //默认数据库配置

    'DB_CONFIG' => array(

        'DB_TYPE' => 'mysql',

        'DB_HOST' => 'localhost',

        'DB_PORT' => '3306',

        'DB_USERNAME' => 'root',

        'DB_PASSWORD' =>’’,

        'DB_NAME' => db1,

    ),

    //默认数据二配置

    'DB_CONFIG2' => array(

        'DB_TYPE' => 'mysql',

        'DB_HOST' => 'localhost',

        'DB_PORT' => '3306',

        'DB_USERNAME' => 'root',

        'DB_PASSWORD' =>’’,

        'DB_NAME' => 'db2',

    ),

);

     在Function.php增加一个函数,用来读取配置文件。

<?php

/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午4:17

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

function getConfig($name){

    return $GLOBALS['config'][$name] ? : '';

}


    4、自动载入函数

        1)、不用自动载入函数,难道要在代码里不断的去include其他的文件吗?

        2)、我们用spl_autoload_register()。从PHP5.1.2引入。摒弃了__autoload()。它的优势是一个项目可以有多个spl_autoload_register()函数。使得项目框架、各种插件(LaneWeChat、PHPMailer、PHPEXCEL等)不会相互冲突

<?php

namespace UniverseInvincibleFrameWork;

/**

 * 自动载入

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:28

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class Autoload{

    public function register(){

        spl_autoload_register(array($this, 'autoload'));

    }


    public function autoload($className){

        $pathArr = explode('\\', $className);

        $filename = array_pop($pathArr);

        $dir = implode(DIRECTORY_SEPARATOR, $pathArr);

        $filename = $dir . '/' . $filename . '.php';

        if(file_exists($filename)){

            require_once $filename;

        }else{

            exit('Error:'.$className.' loading Failed');

        }

    }

}


    5、路由分发

        1)、我们的URL规则:http://www.lanecn.com/index.php/项目名/类名/方法名

        2)、所有的URL,都会去执行index.php。然后路由的作用的是根据不同的URL,来执行不同的Controller。

<?php

namespace UniverseInvincibleFrameWork;

/**

 * 路由

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:29

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class Route{

    /**

     * 分析URL

     */

    public function parse(){

        $pathInfo = !empty($_SERVER['PATH_INFO']) ? explode('/', $_SERVER['PATH_INFO']) : array();

        $appName = !empty($pathInfo[1]) ? $pathInfo[1] : getConfig('DEFAULT_APP_NAME');

        $className = !empty($pathInfo[2]) ? $pathInfo[2] : getConfig('DEFAULT_CONTROLLER');

        $methodName = !empty($pathInfo[3]) ? $pathInfo[3] : getConfig('DEFAULT_METHOD');

        $c = $appName . '\Controller\\' . $className.'Controller';

        $obj = new $c();

        $obj->$methodName();

    }

}


    6、数据库工厂

        1)、大型项目中,我们会用到Mysql、Redis等多种数据库。甚至前期是ACCESS,后期是Mysql/Oracle/SQL SERVER,在切换数据库的过程中,只需要修改一个常量而不需要修改代码。

        2)、根据配置文件中定义的数据库类型,我们选在加载不同的数据库类

<?php

namespace UniverseInvincibleFrameWork\DB;

/**

 * 数据工厂

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:29

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class Db{

    public static function factor($dbConfigKey='DB_CONFIG'){

        //根据参数选择加载不同的数据库配置

        $dbType = strtolower(getConfig($dbConfigKey)['DB_TYPE']);

        switch($dbType){

            case 'mysql':

                $className = 'Mysql';

                break;

            default:

                exit('Error:Database Type');

        }

        $className = 'UniverseInvincibleFrameWork\DB\\'.$className;

        return new $className($dbConfigKey);

    }

}

        3)、项目中的Model文件,继承Model类。该类定义了常用的数据库操作,是所有数据库的抽象类。如增删改查和自定义SQL等。该类使用数据工厂中返回的示例,来操作具体的数据库类。

<?php

namespace UniverseInvincibleFrameWork\DB;

/**

 * 基础Model类,所有的Model文件均继承本类

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午6:35

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class Model implements DbInterface {


    protected $dbConfigKey = null;


    private $_db = null;


    private function _getInstance(){

        if(is_null($this->_db)){

            if(is_null($this->dbConfigKey)){

                $this->_db = DB::factor();

            }else{

                $this->_db = DB::factor($this->dbConfigKey);

            }

        }

        return $this->_db;

    }


    public function close(){

        $this->_getInstance()->close();

    }



    public function query($sql){

        return $this->_getInstance()->query($sql);

    }


    public function fetchAssoc($resource){

        return $this->_getInstance()->fetchAssoc($resource);

    }


    public function select($sql){

        return $this->_getInstance()->select($sql);

    }

}

        4)、最后该编码数据库实例类了。已Mysql为例

<?php

namespace UniverseInvincibleFrameWork\DB;


/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午3:29

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class Mysql implements DbInterface{


    private $_conn = null;


    public function __construct($dbConfigKey='DB_CONFIG'){

        if(is_null($this->_conn)){

            $this->_connect($dbConfigKey);

        }

    }


    private function _connect($dbConfigKey='DB_CONFIG'){

        $dbConfig = getConfig($dbConfigKey);

        $this->_conn = mysqli_connect($dbConfig['DB_HOST'], $dbConfig['DB_USERNAME'], $dbConfig['DB_PASSWORD'], $dbConfig['DB_NAME'], $dbConfig['DB_PORT']);

    }


    public function close(){

        mysqli_close($this->_getInstance());

    }


    public function query($sql){

        $result = mysqli_query($this->_conn, $sql);

        return $result;

    }


    public function fetchAssoc($resource){

        $rowList = array();

        while($row = mysqli_fetch_assoc($resource)){

            $rowList[] = $row;

        }

        return $rowList;

    }


    public function select($sql){

        $result = $this->query($sql);

        $rowList = $this->fetchAssoc($result);

        return $rowList;

    }

}

        5)、补充一点。我们有一个接口类,为了约定各个数据库的实例类的规范,他们都要实现几个关键方法。

<?php

namespace UniverseInvincibleFrameWork\DB;

/**

 * 数据库实例类的接口

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午5:57

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

Interface DbInterface{


    public function close();


    public function query($sql);


    public function fetchAssoc($resource);


    public function select($sql);

}


    7、多数据支持

        1、项目中,常常会遇到既需要数据库A,又需要数据库B。那就需要多数据库支持。

        2、在数据库A中获得用户ID列表,在数据库B中根据用户ID列表获得用户详细信息


    8、多项目支持

        1、框架实现了多项目支持。比如前台、后台、项目三、项目四



四、测试

    1、我们在项目Home中,进行测试。编写两个Model文件。第一个是Home/Model/IndexModel.php 来查询默认数据库的Mysql 版本。第二个是Home/ Model / TestModel.php。来查询数据库2的所有表的名字。

<?php

namespace Home\Model;

use UniverseInvincibleFrameWork\DB\Model;


/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午4:36

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class IndexModel extends Model{

    public function getVersion(){

        $sql = 'SELECT VERSION() as `version`';

        $result = $this->query($sql);

        $result = $this->fetchAssoc($result);

        return $result;

    }

}


<?php

namespace Home\Model;

use UniverseInvincibleFrameWork\DB\Model;


/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午4:36

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class TestModel extends Model{

    public function __construct(){

        $this->dbConfigKey = 'DB_CONFIG2';

    }


    public function getTables(){

        $sql = 'show tables';

        $result = $this->query($sql);

        $result = $this->fetchAssoc($result);

        return $result;

    }

}


    2、写一个Server文件。Home/Server/IndexServer.php来调用刚才写的两个Model文件并返回

<?php

namespace Home\Service;

/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午4:35

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class IndexService{

    public static function getVersion(){

        $ret = array();

        $ret['php_version'] = PHP_VERSION;

        $model = new \Home\Model\IndexModel();;

        $ret['mysql_version'] = $model->getVersion()[0]['version'];

        return $ret;

    }


    public static function getTables(){

        $model = new \Home\Model\TestModel();;

        $ret = $model->getTables();

        return $ret;

    }

}


    3、Home/Controller/IndexController.php编写4个测试示例。

<?php

namespace Home\Controller;

/**

 * Created by lixuan-it@360.cn

 * User: lane

 * Date: 15/8/27

 * Time: 下午4:31

 * E-mail: lixuan868686@163.com

 * WebSite: http://www.lanecn.com

 */

class IndexController{

    public function index(){

        echo 'hello world';

    }


    public function test(){

        echo 'hello 360';

    }


    public function getVersion(){

        $versionList = \Home\Service\IndexService::getVersion();

        echo 'PHP版本:' . $versionList['php_version'].'。Mysql版本:' . $versionList['mysql_version'];

    }


    public function getTables(){

        $tables = \Home\Service\IndexService::getTables();

        var_dump($tables);

    }

}


    4、结果:

        1)、浏览器运行http://framework/index.php/Home/Index/index。正常输出:“hello world”

        2)、浏览器运行http://framework/index.php/Home/Index/test。正常输出:“hello 360”

        3)、浏览器运行http://framework/index.php/Home/Index/getVersion。正常输出:“PHP版本:5.5.27。Mysql版本:5.6.17”

        4)、浏览器运行http://framework/index.php/Home/Index/getTables。正常输出数据库2的所有表名 


        本文介绍PHP生成PDF。我们使用TCPDF开源插件,实现PHP生成PDF文档。可以插入图片、HTML、链接、表格、柱状图折线图等PHP动态生成PDF的功能。

        PHP的PECL扩展有一个叫做pdflib,并且维护到了2014年1月,PDFLib库对于个人是免费的,对于商业产品需要购买许可。并且使用相对复杂。因此排除。

        本文介绍一款插件,TCPDF!官网http://www.tcpdf.org。下载后在代码中引入即可使用。无需编译/安装其他的扩展。TCPDF的下载包和官网都会提供大量的示例和几十个字体(只是除个别外中文都不能用...)。采用LGPL license开源协议。

        下面直奔主题。在官网下载后。假设放在了/var/www/目录下。

//第一步肯定是引入TCPDF的入口文件

require_once '/var/www/tcpdf/tcpdf.php';


//实例化

$pdf = new TCPDF('P', 'mm', 'A4', true, 'UTF-8', false);

// 设置文档信息

$pdf->SetCreator('Lane');

$pdf->SetAuthor('Lane');

$pdf->SetTitle('PHP生成PDF');

$pdf->SetSubject('PHP动态生成PDF文件');

$pdf->SetKeywords('PHP PDF TCPDF');


//设置页眉信息 参数分别是LOGO地址,LOGO大小,两行标题,标题颜色,分割线颜色。。颜色是RGB

$pdf->SetHeaderData('/var/www/tcpdf/examples/images/tcpdf_logo.jpg', 30, 'PHP生成PDF', 'PHP如何生成PDF文件', array(0,0,0), array(0,0,0));

//设置页脚信息

$pdf->setFooterData(array(0,0,0), array(0,0,0));

// 设置页眉和页脚字体

$pdf->setHeaderFont(Array('stsongstdlight', '', '12'));

$pdf->setFooterFont(Array('helvetica', '', '8'));

//设置默认等宽字体

$pdf->SetDefaultMonospacedFont('courier');

//设置间距

$pdf->SetMargins(15, 27, 15);

$pdf->SetHeaderMargin(5);

$pdf->SetFooterMargin(10);

//设置分页

$pdf->SetAutoPageBreak(TRUE, 15);

//设置图片比例

$pdf->setImageScale(1.25);

//将页眉页脚的信息输出出来。

$pdf->AddPage();


//设置字体 - 正文标题的哦。B是加粗,15是大小

$pdf->SetFont('stsongstdlight', 'B', 15);

$pdf->Write(20, 'PHP如何动态生成PDF', '', 0, 'C', true, 0, false, false, 0);


//设置字体 - 正文内容的哦。B是加粗,15是大小

$pdf->SetFont('stsongstdlight', '', 10);

//L是左对齐,R是右对齐,C是居中对齐。

$pdf->Write(0, $content,'', 0, 'L', true, 0, false, false, 0);


//输出PDF。第二个参数默认是I,是浏览器预览。D是下载

$pdf->Output('PHP_TO_PDF.pdf', 'I');


        复制并执行上面的代码,会发现浏览器打开了PDF文件预览(如果你的浏览器不是IE的话)。把Output的第二个参数换成D,就可以下载了。


        说到这里,基本是完成了。但是有个问题哦,你会发现字体很别扭,特别丑。我们可以换个字体,《droidsansfallback》。该字体并未自带。可以通过Google百度找到下载的地方,或者在http://sourceforge.net/projects/hawebs/files/Assistance/PHP/Droid%20Sans%20Fallback%20-%20PHP.zip/download  。 下载解压后,将droidsansfallback.php、droidsansfallback.z以及droidsansfallback.ctg.z这三个文件复制到 tcpdf/fonts/目录下。然后将代码中的stsongstdlight替换成droidsansfallback即可。在执行,就会发现字体好看多了。。当然,可以用TCPDF自带的tcpdf_addfont.php来将其他字体转换成TCPDF识别的字体,再移入tcpdf/fonts/目录下。比如微软雅黑什么的。