本文系转载,是在phphub上看到的,作者:吕文翰,JohnLui,生于1993年,真是青年才俊啊
原文应该是在这里:http://lvwenhan.com/
(一)视图装载
本教程示例代码见 https://github.com/johnlui/My-First-Framework-based-on-Composer
回顾
经过了上一个 系列教程 《利用 Composer 一步一步构建自己的 PHP 框架》 ,我们组建了一个具有 路由 、 MVC 架构 和 ORM 功能的基础框架 MFFC 。接下来我们继续完善这个项目。
我们先从目前用着 最不爽 的地方——视图装载下手。我们将封装一个视图装载类,让它来帮我们装载视图,并把变量传递进视图。这个类将只暴露出来几个简单的接口,让我们在控制器里面用的爽,让我们一边写代码一边笑。真的笑,笑出声。:-D
正文
构思
视图装载类要做的工作其实很简单:
1. 根据视图名称找到视图文件,支持文件夹
2. 更加方便,更加优雅地把变量的值传递进视图
本文中我们将不会不引入模板引擎,只做装载文件和传递变量的功能。
基础准备
我们要引入视图装载器,这就正式打开了组件化的大门,所以我们需要做一些准备工作。
启动流程组件化
将 public/index.php 里面的代码分离一部分到启动器(bootstrap),新建 MFFC/bootstrap.php 文件:
<?php use Illuminate\Database\Capsule\Manager as Capsule; // 定义 BASE_PATH define('BASE_PATH', __DIR__); // Autoload 自动载入 require BASE_PATH.'/vendor/autoload.php'; // Eloquent ORM $capsule = new Capsule; $capsule->addConnection(require BASE_PATH.'/config/database.php'); $capsule->bootEloquent();
修改 public/index.php 为:
<?php // 定义 PUBLIC_PATH define('PUBLIC_PATH', __DIR__); // 启动器 require PUBLIC_PATH.'/../bootstrap.php'; // 路由配置、开始处理 require BASE_PATH.'/config/routes.php';
这时候我们就完成了 入口文件 和 启动器 的分离,并定义了两个全局常量 BASE_PATH 和 PUBLIC_PATH 。
在这里我们需要特别注意一点:“引入路由配置文件” 这一步并不只是简单地引入了一个配置文件,路由文件的最后一行 Macaw::dispatch(); 才是 真正执行某个控制器中某个 function 的地方,所有准备条件都应该在载入路由文件之前完成,例如 Eloquent 的初始化,还有以后我们要使用的 Composer 包的初始化等等。
引入错误页面提示组件
我们选择 filp/whoops 作为我们错误提示组件包。
修改 composer.json :
"require": { "codingbean/macaw": "dev-master", "illuminate/database": "*", "filp/whoops": "*" },
运行 composer update ,然后在 bootstrap.php 的最后添加:
// whoops 错误提示 $whoops = new \Whoops\Run; $whoops->pushHandler(new \Whoops\Handler\PrettyPageHandler); $whoops->register();
刷新 http://127.0.0.1:81 ,你得到的应该还是这个页面:
下面我们将增加路由配置中 无匹配项 的错误页面,修改 config/routes.php :
<?php use NoahBuscher\Macaw\Macaw; Macaw::get('', 'HomeController@home'); Macaw::get('fuck', function() { echo "成功!"; }); Macaw::$error_callback = function() { throw new Exception("路由无匹配项 404 Not Found"); }; Macaw::dispatch();
现在访问一个随意输入的 URL,例如 http://127.0.0.1:81/asd ,我们会看到以下画面:
是不是有一种很熟悉的感觉!
很不幸,这个错误提示包正是 Laravel 采用的那个,所以,我们可爱的 MFFC 框架在长大以后还是成了 Laravel 的样子。%>_<%
实现装载器
完成基础准备以后我们正式开始制造视图装载器。
视图装载器是一个可插拔组件,我们应该把所有可插拔组件全部归到一处,在 MFFC 中建议放在 MFFC/services 下。
CI 框架提供的基础组件库叫 helpers ,Laravel 使用 illuminate/support 包提供一些可重用的系统函数。实际上 “illuminate/support” 这个包已经被我们的 ORM 包 “illuminate/database” 依赖了,现在 MFFC 框架里面已经可以直接使用。,这个包的中文文档见:http://laravel-china.org/docs/helpers
我们并没有像 CI 框架那样把视图装载器放到系统核心,有以下两个原因:
- 基于命名空间与自动加载的调用方式更加节省资源
- 在移动互联网和大前端愈演愈烈的时代,后端越来越 API 化、 json 化。很多时候都不到视图,没有必要再增加无畏的消耗。
下面开始着手实现视图装载器。
新建 MFFC/services 文件夹,并修改 composer.json 把这个文件夹下的所有类自动归入根命名空间:
"autoload": { "classmap":[ "app/controllers", "app/models", "services" ] }新建 services/View.php 文件,内容如下:
<?php /** * \View */ class View { const VIEW_BASE_PATH = '/app/views/'; public $view; public $data; public function __construct($view) { $this->view = $view; } public static function make($viewName = null) { if ( ! $viewName ) { throw new InvalidArgumentException("视图名称不能为空!"); } else { $viewFilePath = self::getFilePath($viewName); if ( is_file($viewFilePath) ) { return new View($viewFilePath); } else { throw new UnexpectedValueException("视图文件不存在!"); } } } public function with($key, $value = null) { $this->data[$key] = $value; return $this; } private static function getFilePath($viewName) { $filePath = str_replace('.', '/', $viewName); return BASE_PATH.self::VIEW_BASE_PATH.$filePath.'.php'; } public function __call($method, $parameters) { if (starts_with($method, 'with')) { return $this->with(snake_case(substr($method, 4)), $parameters[0]); } throw new BadMethodCallException("方法 [$method] 不存在!."); } }运行 composer dump-autoload ,完成以后,我们就可以在控制器中直接调用这个类了。
修改 controllers/HomeController.php :
<?php /** * \HomeController */ class HomeController extends BaseController { public function home() { $this->view = View::make('home')->with('article',Article::first()) ->withTitle('MFFC :-D') ->withFuckMe('OK!'); } }修改 controllers/BaseController.php :
<?php /** * \BaseController */ class BaseController { protected $view; public function __construct() { } public function __destruct() { $view = $this->view; if ( $view instanceof View ) { extract($view->data); require $view->view; } } }修改 app/views/home.php :
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title><?php echo $title ?></title> </head> <body> <div class="article"> <h1><?php echo $article['title'] ?></h1> <div class="content"> <?php echo $article['content'] ?> </div> </div> <ul class="fuckme"> <li>Fuck Me !</li> <li> <?php echo $fuck_me ?> </li> </ul> </body> </html>
刷新,你将看到以下页面:
至此,视图装载器实现完成。
下面我大致说一下设计视图装载器的基本思路:
- 这个视图装载器类模仿了 Laravel 的 View 类,它实现了一个静态方法 make ,接受视图名称作为参数,以 . 作为目录的间隔符。
- make 静态方法会检查视图名称是否为空,检查视图文件是否存在,并给出相应的异常。这就是我们引入异常处理包的原因。
- 视图名称合法且文件存在时,实例化一个 View 类的对象,返回。
- 使用 with(‘key’, $value) 或者优雅的 withKey($value) 来给这个 View 对象插入要在视图里调用的变量。 withFuckMe($value) 将采用蛇形命名法被转化成 $fuck_me 供视图使用。
- 最终组装好的 View 对象会被赋给 HomeController 的成员变量 $view ,这个变量是从 BaseController 中继承得来。
- 父类 BaseController 中的析构函数 __destruct() 将在 function home() 执行完成后处理这个成员变量: extract 出视图要用到的变量, require 视图文件,将最终运算结果发送给浏览器,流程结束。
(二)发送邮件
回顾
上一篇文章中,我们手工建造了一个简易的视图加载器,顺便引入了错误处理包,让我们的 MFFC 框架在 M、V、C 三个方面都达到了“好用”的水平。View 是一个可插拔组件,在本篇文章中我们要创建另一个可插拔组件——邮件发送模块。
正文
我们采用 ‘nette/mail’ 包作为我们的邮件发送基础模块,在它的基础上封装一个 ‘Mail’ 类,暴露出简洁的 API 给控制器使用,下面我们正式开始。
引入 ‘nette/mail’ 包,修改 ‘composer.json’:
"require": { "codingbean/macaw": "dev-master", "illuminate/database": "*", "filp/whoops": "*", "nette/mail": "*" },运行 ‘composer update’,等待安装完成。’nette/mail’ 的文档位于:http://doc.nette.org/en/2.2/mailing 让我们阅读它,然后设计 Mail 类:
新建 ‘services/Mail.php’ 文件,内容如下:
<?php use Nette\Mail\Message; /** * \Mail */ class Mail extends Message { public $config; // [String] e-mail protected $from; // [Array] e-mail list protected $to; protected $title; protected $body; function __construct($to) { $this->config = require BASE_PATH.'/config/mail.php'; $this->setFrom($this->config['username']); if ( is_array($to) ) { foreach ($to as $email) { $this->addTo($email); } } else { $this->addTo($to); } } public function from($from=null) { if ( !$from ) { throw new InvalidArgumentException("邮件发送地址不能为空!"); } $this->setFrom($from); return $this; } public static function to($to=null) { if ( !$to ) { throw new InvalidArgumentException("邮件接收地址不能为空!"); } return new Mail($to); } public function title($title=null) { if ( !$title ) { throw new InvalidArgumentException("邮件标题不能为空!"); } $this->setSubject($title); return $this; } public function content($content=null) { if ( !$content ) { throw new InvalidArgumentException("邮件内容不能为空!"); } $this->setHTMLBody($content); return $this; } }Mail 类和 View 类工作的方式基本一致:
$this->mail = Mail::to(['ooxx@gmail.com', 'ooxx@qq.com']) ->from('MotherFucker <ooxx@163.com>') ->title('Fuck Me!') ->content('<h1>Hello~~</h1>');上面这段代码位于 HomeController 中, ‘View::make()’ 那行代码的下面。
新建 ‘MFFC/config/mail.php’,请自行替换邮件地址和密码:
<?php return [ 'host' => 'smtp.163.com', 'username' => 'ooxx@163.com', 'password' => 'password', 'secure' => '' ];Mail 和 View 一样也在 BaseController 的析构函数 __destruct() 函数中处理,现在这个 function 长这样:
public function __destruct() { $view = $this->view; if ( $view instanceof View ) { extract($view->data); require $view->view; } $mail = $this->mail; if ( $mail instanceof Mail ) { $mailer = new Nette\Mail\SmtpMailer($mail->config); $mailer->send($mail); } }OK,准备的差不多了,运行 ‘composer dump-autoload’ 把 Mail 类加入自动加载,刷新页面!
如果你看到以上页面,恭喜你!邮件发送成功了!
赶快去检查一下收件箱有木有邮件!:-D 这次页面加载可能会稍慢,因为邮件是同步发送的。异步的队列系统我们会在以后讲到。
分析
邮件发送的整体流程想必大家已经轻车熟路了,现在主要叙述一下 Mail 类的设计过程:
- 邮件发送的核心参数是 ‘目标地址’,即邮件要发送到的 E-mail 地址,所以我们设计 Mail::to(‘oo@xx.me’) 作为发送的 ‘触发 API’。
- 目前我们采用最简单的 ‘SMTP’ 方式发送邮件,文档在 这里。配置文件放置在 ‘MFFC/config/mail.php’ 中,依旧返回一个数组。
- Mail 类继承了 ‘Nette\Mail\Message’ 类。’Mail::to()’ 的时候创建一个 Mail 类的实例(对象)并返回,这时候其实 ‘BaseController’ 中的析构函数中的代码已经会被触发并处理这个对象了。默认的发送人是从配置文件中读取的 ‘username’。
- ‘Mail::to()’ 支持 字符串 或者数组作为参数,可以一次发送一封或多封邮件。
- ‘from()’、’title()’ 和 ‘content()’ 方法用于丰富邮件内容。’content()’ 方法可以直接传递 HTML 代码。
- ‘from()’ 配置不一定都能够成功,部分邮件服务商不支持修改发送人地址。
- 这个变量全部组装完成后,被赋值给控制器的 ‘$mail’ 成员变量,然后被析构函数处理,邮件被发送,成功后页面代码被发送回客户端,流程结束。
(三)Redis缓存
回顾
上两篇文章中我们完成了 View 视图加载类和 Mail 邮件发送类的设计,完成了两个可插拔组件。本篇文章中我们将构建另一个可插拔组件——’Redis 接口’,并使用它构建 ‘MFFC’ 的高速缓存服务。
正文
Redis 简介
‘Redis’ 是一个高性能的 ‘key-value’ 数据库,其 ‘value’ 支持 ‘String’、’Map(Hash)’、’list’、’set’ 和 ‘sorted sets’,中文翻译为 字符串、字典(哈希,在’世界上最好的语言PHP’ 中属于 ‘数组’ 的一部分)、列表、集合和有序集合。
我们可以用 Redis 作为高速缓存,存放系统经常需要访问的数据。相比使用文件作为缓存,Redis 拥有更高的性能、更好地可维护性和更强大的操作 API。
Redis 安装
Redis 官网为 http://redis.io/ ,可以很容易地下载安装并在命令行直接启动,它默认监听 ‘6379’ 端口。
Redis 服务端装好并启动之后,并不能写一行代码直接开始使用了,我们还需要安装 ‘客户端’。目前有两种方式比较推荐:
1. 安装 PHP 的 Redis 扩展。
2. 使用 nrk/predis 包。
注意:作为 PHP 扩展安装时,请先打印出 ‘phpinfo()’,找到 ‘Loaded Configuration File’ 这一项,编辑这个 ‘php.ini’ 才有效。别忘了 ‘重启’ Apache 或者 php-fpm。
Redis 使用
我们在此选择 ‘nrk/predis’ 包作为 Redis 驱动。编辑 ‘composer.json’:
"require": { "codingbean/macaw": "dev-master", "illuminate/database": "*", "filp/whoops": "*", "nette/mail": "*", "predis/predis": "*" },运行 ‘composer update’,等待安装完成。
然后我们就要开始构建 Redis 类了,新建 ‘services/Redis.php’:
<?php use Predis\Client; /** * \Redis */ class Redis { const CONFIG_FILE = '/config/redis.php'; protected static $redis; public static function init() { self::$redis = new Client(require BASE_PATH.self::CONFIG_FILE); } public static function set($key,$value,$time=null,$unit=null) { self::init(); if ($time) { switch ($unit) { case 'h': $time *= 3600; break; case 'm': $time *= 60; break; case 's': case 'ms': break; default: throw new InvalidArgumentException('单位只能是 h m s ms'); break; } if ($unit=='ms') { self::_psetex($key,$value,$time); } else { self::_setex($key,$value,$time); } } else { self::$redis->set($key,$value); } } public static function get($key) { self::init(); return self::$redis->get($key); } public static function delete($key) { self::init(); return self::$redis->del($key); } private static function _setex($key,$value,$time) { self::$redis->setex($key,$time,$value); } private static function _psetex($key,$value,$time) { self::$redis->psetex($key,$time,$value); } }新建配置文件 ‘config/redis.php’:
<?php return [ 'host' => '127.0.0.1', 'port' => 6379 ];然后我们就可以开始测试啦~ 在 ‘HomeController’ 中增加:
Redis::set('key','value',5,'s'); echo Redis::get('key');运行一次后将上面一行注释掉,不断刷新,看 ‘value’ 是否会在设定的时间结束后从页面上消失。
代码分析
1. 我们创建了 ‘\Redis’ 类,提供了 ‘::set()’、’::get()’ 和 ‘::delete()’ 三个静态方法,用于新增、获取和删除一对 ‘key-value’。
2. ‘::set()’ 方法支持设定 ‘key-value’ 键值对的生命周期,并支持 ‘h’、’m’、’s’ 和 ‘ms’ 四个单位,代表时、分、秒和毫秒,这样就可以方便地用于缓存了。
3. 严格意义上讲,缓存类至少还需要一个 ‘判断 key 是否还存在’ 的基本操作,所以这个类还只是一个略微强大的 Redis 接口。
4. 建造缓存接口作为练习,大家可以尝试自己动手写一下。选择新建 Cache 类或者在 Redis 类中新增接口均可。
【完结】