利用 Composer 完善自己的 PHP 框架

本文系转载,是在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 ,你得到的应该还是这个页面:

Image

下面我们将增加路由配置中  无匹配项  的错误页面,修改  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 ,我们会看到以下画面:

Image

是不是有一种很熟悉的感觉!

很不幸,这个错误提示包正是 Laravel 采用的那个,所以,我们可爱的  MFFC  框架在长大以后还是成了  Laravel  的样子。%>_<%

实现装载器

完成基础准备以后我们正式开始制造视图装载器。

视图装载器是一个可插拔组件,我们应该把所有可插拔组件全部归到一处,在 MFFC 中建议放在  MFFC/services  下。

CI 框架提供的基础组件库叫  helpers ,Laravel 使用  illuminate/support  包提供一些可重用的系统函数。实际上 “illuminate/support” 这个包已经被我们的 ORM 包 “illuminate/database” 依赖了,现在 MFFC 框架里面已经可以直接使用。,这个包的中文文档见:http://laravel-china.org/docs/helpers

我们并没有像 CI 框架那样把视图装载器放到系统核心,有以下两个原因:

  1. 基于命名空间与自动加载的调用方式更加节省资源
  2. 在移动互联网和大前端愈演愈烈的时代,后端越来越 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>

 

刷新,你将看到以下页面:

Image

至此,视图装载器实现完成。


下面我大致说一下设计视图装载器的基本思路:

  1. 这个视图装载器类模仿了 Laravel 的 View 类,它实现了一个静态方法  make ,接受视图名称作为参数,以  .  作为目录的间隔符。
  2. make 静态方法会检查视图名称是否为空,检查视图文件是否存在,并给出相应的异常。这就是我们引入异常处理包的原因。
  3. 视图名称合法且文件存在时,实例化一个 View 类的对象,返回。
  4. 使用  with(‘key’, $value)  或者优雅的  withKey($value)  来给这个 View 对象插入要在视图里调用的变量。 withFuckMe($value)  将采用蛇形命名法被转化成  $fuck_me  供视图使用。
  5. 最终组装好的 View 对象会被赋给  HomeController  的成员变量  $view ,这个变量是从  BaseController  中继承得来。
  6. 父类  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 类加入自动加载,刷新页面!

Image

如果你看到以上页面,恭喜你!邮件发送成功了!

赶快去检查一下收件箱有木有邮件!:-D 这次页面加载可能会稍慢,因为邮件是同步发送的。异步的队列系统我们会在以后讲到。

分析

邮件发送的整体流程想必大家已经轻车熟路了,现在主要叙述一下 Mail 类的设计过程:

  1. 邮件发送的核心参数是 ‘目标地址’,即邮件要发送到的 E-mail 地址,所以我们设计 Mail::to(‘oo@xx.me’) 作为发送的 ‘触发 API’。
  2. 目前我们采用最简单的 ‘SMTP’ 方式发送邮件,文档在 这里。配置文件放置在 ‘MFFC/config/mail.php’ 中,依旧返回一个数组。
  3. Mail 类继承了 ‘Nette\Mail\Message’ 类。’Mail::to()’ 的时候创建一个 Mail 类的实例(对象)并返回,这时候其实 ‘BaseController’ 中的析构函数中的代码已经会被触发并处理这个对象了。默认的发送人是从配置文件中读取的 ‘username’。
  4. ‘Mail::to()’ 支持 字符串 或者数组作为参数,可以一次发送一封或多封邮件。
  5. ‘from()’、’title()’ 和 ‘content()’ 方法用于丰富邮件内容。’content()’ 方法可以直接传递 HTML 代码。
  6. ‘from()’ 配置不一定都能够成功,部分邮件服务商不支持修改发送人地址。
  7. 这个变量全部组装完成后,被赋值给控制器的 ‘$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 类中新增接口均可。


【完结】

 

By | 2017-06-07T13:59:41+00:00 四月 1st, 2015|大宝推荐|利用 Composer 完善自己的 PHP 框架已关闭评论

Share This Story, Choose Your Platform!

About the Author:

Toggle Sliding Bar Area

墨尔本网站制作

墨尔本个人网站制作 / 墨尔本婚纱摄影网站制作 / 墨尔本电子商务网站制作 / 墨尔本公司网站制作 / 墨尔本房地产网站制作

澳洲主机/服务器

墨尔本主机 / 墨尔本网站服务器 / 悉尼主机 / 悉尼网站服务器 / 香港主机 / 香港网站服务器