我以自己的oneblog体验了一下tp5 beta 版的使用,写下最小修改的升级方式。
# 目的
告诉大家如何去移植旧项目到新版tp5 beta中去。以及可能遇到的问题,和老杨是怎么解决的。
# 前提
做正确的移植之前,我们得熟读一下本手册,这样才能少走弯路。
# 几个巨大的变化
## 命名规范
一开始说是PSR0 后来大家协作时说规范是PSR2, 有人建议Sublime 装一个phpfmt 插件,可是我装了提示php版本低于5.6 用不了。不折腾了。
建议大家看看之前的命名规范章节。 主要是命名空间,目录和文件名小写,分割用_,但是类名要大写首字母驼峰式。开始老杨也不理解,为什么类名不也小写。后来一想,类名一小写。就难和文件和目录区分开了。
## 配置不合并
以往我们记得有个Common模块, 里面有公共函数、公共配置,然后其他模块的会先加载这个配置和函数然后去跟当前模块的合并。现在不行了。现在的公共配置不放在Common里,放在application APP_PATH根目录下。什么config.php、database.php、common.php啦。后来问老大那现在的Common 干嘛用,他说放公共的类,比如公共的model、behavior
## Traits
新框架用了php5.4开始添加的Traits功能,老杨看了下,就是用于多继承的,使得代码更精简,比方 原来我们有高级模型、视图模型,然后,我们一个类既有视图模型、又有高级模型的功能,要实现这样的怎么办,定义一个第三方模型,复制这两个模型里的代码。。 现在有了Traits,我们可以 通过use 关键字,将Traits 实现的功能,直接在模型里 复用一下。框架新增了T函数是为了兼容php5.4版本的,5.5的可以直接引用。
因此,老大在架构上把一些常用的自动完成、高级模型给分离开到traits目录里去了,这带来了一些不便。后面我会讲如何去使用,达到以前的效果。
## 调试
以前的调试工具条被舍弃了,换成了专门调试api的不影响页面输出的SocketLog工具。当然也不说不能用,只是有一些不便:依赖网络、空值不输出。
## 耦合低了,很多东西独立开来了,比如视图 以前我们控制器里可以直接display、error、succes。现在必须依赖视图类,因为老大认为面向api的框架,很少需要视图。当然如果能用视图的话,原先的视图功能还是完整的,只不过使用上不方便。
# 移植的步骤
## 入口+框架
首先去<https://github.com/top-think/think> git 工具下载一份 beta版 tp5。 放入自己的环境里去。
然后修改入口文件,改为以下的:
~~~
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2015 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
// 应用入口文件
// 定义项目路径
define('APP_PATH', './application/');
define('ONETHINK_ADDON_PATH', './addons/');
// 开启调试模式
define('APP_DEBUG', true);
define('APP_HOOK', true);
define('SLOG_ON', true);
// define('BIND_MODULE','home');
// define('BIND_CONTROLLER','index');
// 加载框架引导文件
require './thinkphp/start.php';
~~~
## 自动生成
默认的示列应用APP_PATH下带了一个build.php 自动生成配置文件,我们可以修改一下,用于生成自己以前项目的目录结构,不过有点繁琐。
比方老杨以前的tp3.2的application下的结构是这样子的:
![](https://box.kancloud.cn/2015-12-19_5674d7727cf1b.png)
添加自动完成时,一个模块一个模块的加。然后依次是模块对应的子目录`__dir__`、`__file__`、controller、model、view 之类的子目录。比如老杨的admin模块:
~~~
'admin' => [
'__file__' => [],
'__dir__' => ['controller', 'view'],
'controller' => ['Addons', 'Article', 'Cate', 'Common', 'Config', 'System', 'Theme', 'User'],
'view' => ['addons/index', 'article/add', 'cate/add', 'config/index', 'public/base', 'system/index', 'tags/index', 'theme/index', 'user/index'],
],
~~~
因为模块里的视图模板文件太多了,一个个手写很累,所以老杨只写了一个控制器里的一个模板,生成好后,手动复制过来整个目录的模板即可(因为模板一般都小写的,不用改文件名)。
自动生成控制器和模型的目的是节省时间,因为变化太大了:没有.class.php后缀,首字母要小写,对应命名空间也变了。能偷懒为什么不用呢。其实如果针对移植升级,可以写一个小脚本,扫描旧目录去生成对应新版的。
PS:sublime 对于rename这件事有个bug,就是如果有的目录或者文件更改大小写,然后这个目录和文件就打不开了。即使用菜单的刷新功能更也没用。只能手动关闭整个项目,再重新打开st。老杨发现重命名时先改小写的,同时多加一个s后 先重命名一次,然后在去掉s 百分百不会触发bug。
PS:自动生成每次访问都会生成,如果你后来想删除某些文件了。记得把自动生成的也改一下,要是node 可以用gulp之类的watch 一下。php 还是人工吧。
完整的oneblog 生成:
~~~
return [
// 生成运行时目录
'runtime' => [
'__dir__' => ['cache', 'log', 'temp'],
],
'admin' => [
'__file__' => [],
'__dir__' => ['controller', 'view'],
'controller' => ['Addons', 'Article', 'Cate', 'Common', 'Config', 'System', 'Theme', 'User'],
'view' => ['addons/index', 'article/add', 'cate/add', 'config/index', 'public/base', 'system/index', 'tags/index', 'theme/index', 'user/index'],
],
'common'=>[
'__file__' => [],
'__dir__' => ['behavior', 'controller', 'model', 'api'],
'controller' => ['Addon'],
'model' => ['Addons', 'Article', 'Cate', 'Config', 'File', 'Hooks', 'Picture', 'Tree', 'Url'],
],
'home'=>[
'__file__' => ['config.php', 'common.php'],
'__dir__' => ['widget', 'controller', 'view'],
'controller' => ['Addons', 'Api', 'Error', 'Index'],
'view' => ['index/index', 'widget/archive'],
'widget' =>['Common']
],
// 。。。 其他更多的模块定义
];
~~~
## 更新数据库配置、slog和其他配置
首先数据库的配置独立出来再APP_PATH下的database.php。记得新版 咱数组用[] 来写,多精简。
然后就是老几项了。hostname、username、password 之类的。
slog 的配置:
~~~
'slog' => [
'enable' => true, //是否记录日志的开关
'host' => '111.202.76.133',
//是否显示利于优化的参数,如果允许时间,消耗内存等
'optimize' => true,
'show_included_files' => true,
'error_handler' => true,
//日志强制记录到配置的client_id
'force_client_id' => 'XXX',
//限制允许读取日志的client_id
'allow_client_ids' => ['XXX'],
],
~~~
新版的配置文件中键名都是小写。老的用来写旧版的和自定义配置吧。
注意的是入口要定义 SLOG_ON 常量为true才起作用,老杨也是追了源码才知道的。
其他配置项,可以参考框架的convention.php文件
![](https://box.kancloud.cn/2015-12-19_5674d77296a1f.png)
有对应的注释。
PS:有的类的构造方法里的配置,需要写在对应的命名空间里,如
![](https://box.kancloud.cn/2015-12-19_5674d773443eb.png)
template的配置,并不是不可变,而是要
~~~
'template'=>[
'compile_type'=>'sae'
]
~~~
这样去改变,老杨也是测试sae试出来的。
## 移植模块目录里的函数、配置、tags
公共函数移出来,对应模块的函数复制到对应模块的common.php中,配置也是对应的config.php tags.php也是,只不过现在没有多余的Common和Config目录了,都是单文件。
## 将视图复制过来,修复命名上的错误
将对应的模块的view目录复制过来。基本上就能用了,但是注意一件事,include 和 extend 的name 对应的路径,只支持控制器/模板名了,且区分大小写。因此Public/Common 和public::common都是不对的。必须public/common
等搞定视图输出,时测试一些用法。老杨测试出include 属性变量没解析对和switch的 case 多层嵌套有问题,当然已经修复了,你们可以测测看有什么不兼容的。去提issue。
## 局部访问 测试bug 差异化
控制器的一些方法、框架函数之类的。
常见单字母函数,框架还是留了可以兼容。如D('Article') 会找当前模块的,找不到去找common/model下的article.php就是Article类。
# 遇到的问题
## 公共模块的配置不合并了?
参见上面的介绍,位置变了。
## 视图里一些方法不见了
为了能更好的测试,及系统流程的变化,现在tp的控制器必须有返回值。(特殊方法除外)。
返回给Response类(去thinkphp\libary\think下找),
所以官方的例子是 new \think\View 自己fetch 自己return。
后来群友提出来以前方便的succes和error 自动判断ajax 不见。我觉得每次自己实例化视图类很麻烦,老大就用Traits支持了 controller类里 的视图替换字符串变了fetch、show、assign 方法:
![](https://box.kancloud.cn/2015-12-19_5674d7745495e.png)
因此我们只需要有视图的控制器继承 \think\controller类就行了和之前一样的assign、show、及抛弃display 用 fetch 替代。return 模板字符串。
## 配置精简了
大家看convention.php 文件就知道配置少了很多了。因为很多行为也没了。
## 分页类没有了
为了内核的精简,老大没有加,老杨手动移植了一个放到org和ot里 ot 自定义了样式。其他的类你看着放吧!
## $this->redirect 没了
去response类看看 可以`\think\response::redirect`
## 模型默认很多快捷操作没有了?
这个很头痛,比方说以前M('article')->count()、 M('Article')->getField 和getFieldBy 都不能用了。
现在只能 D后用,然后在对应的模型里,使用 traits。
如
~~~
<?php
namespace common\model;
T('model/adv');
T('model/auto');
class Article extends \think\Model{
use \traits\model\adv;
use \traits\model\auto;
public function _initialize(){
/* 自动验证规则 */
$this->validate = array(
array('name', '/^[a-zA-Z]\w{0,39}$/', '文档标识不合法', self::VALUE_VALIDATE, 'regex', self::MODEL_BOTH),
array('name', '', '标识已经存在', self::VALUE_VALIDATE,'unique'),
array('title', 'require', '标题不能为空', self::MUST_VALIDATE, 'regex', self::MODEL_BOTH),
array('title', '1,80', '标题长度不能超过80个字符', self::MUST_VALIDATE, 'length', self::MODEL_BOTH),
array('description', '1,140', '简介长度不能超过140个字符', self::VALUE_VALIDATE, 'length', self::MODEL_BOTH),
array('cate_id', 'require', '分类不能为空', self::MUST_VALIDATE , 'regex', self::MODEL_INSERT),
array('cate_id', 'require', '分类不能为空', self::EXISTS_VALIDATE , 'regex', self::MODEL_UPDATE),
);
/* 自动完成规则 */
$this->auto = array(
array('uid', 'is_login', self::MODEL_INSERT, 'function'),
array('title', 'htmlspecialchars', self::MODEL_BOTH, 'function'),
array('content', 'base_encode', self::MODEL_BOTH, 'callback'),
array('description', 'htmlspecialchars', self::MODEL_BOTH, 'function'),
array('link_id', 'getLink', self::MODEL_BOTH, 'callback'),
array('view', 0, self::MODEL_INSERT, 'string'),
array('comment', 0, self::MODEL_INSERT, 'string'),
array('create_time', 'getCreateTime', self::MODEL_BOTH,'callback'),
array('update_time', NOW_TIME, self::MODEL_BOTH, 'string'),
array('status', '1', self::MODEL_INSERT, 'string'),
);
}
~~~
有自动完成和自动验证的用 traits/auto。 有after 后置操作,和快捷操作的 用 traits/adv。 发现自己的查询不支持了,先去 traits/model 里找找。
因此有时为了一个简单的复杂查询得建一个简单模型:
如user
~~~
<?php
namespace common\model;
T('model/adv');
class User extends \Think\Model{
use \traits\model\adv;
}
~~~
注意的是新版的 auto 和 valiate 不带_ 了,我的做法是 _initialize 方法里 属性赋值或者直接= 以前的数组。
## 模板路径替换不能用了?
因为新版 现在没有任何内置行为。
开始我是移植旧的ContentReplace行为。`parse_str` 这个配置。
配置里将以前的TPML_PARSE_STRING 替换成这个键名即可。
值的注意的是 以前3.2版 dispatch 里定义的很多常量没了, `__ROOT__` 之类的,我挑了3个常用的 `__ROOT__`、`__APP__`、`__SELF__`
~~~
if (!IS_CLI) {
// 当前文件名
if (!defined('_PHP_FILE_')) {
if (IS_CGI) {
//CGI/FASTCGI模式下
$_temp = explode('.php', $_SERVER['PHP_SELF']);
define('_PHP_FILE_', rtrim(str_replace($_SERVER['HTTP_HOST'], '', $_temp[0] . '.php'), '/'));
} else {
define('_PHP_FILE_', rtrim($_SERVER['SCRIPT_NAME'], '/'));
}
}
if (!defined('__ROOT__')) {
$_root = rtrim(dirname(_PHP_FILE_), '/');
define('__ROOT__', (($_root == '/' || $_root == '\\') ? '' : $_root));
}
define('PHP_FILE', _PHP_FILE_);
}
if(!defined('__APP__'))
define('__APP__', strip_tags(PHP_FILE));
// URL常量
if(!defined('__SELF__'))
define('__SELF__', strip_tags($_SERVER[C('URL_REQUEST_URI')]));
~~~
将旧版的移植到 config.php return 上面。本来是想放common.php公共函数里的,结果不起作用。
## 插件不能用了?
由于oneblog 移植了ot的插件机制,所以也得研究一下了。
插件依赖钩子,所以入口要定义HOOK_ON 常量。
然后头疼的就是命名空间了。
3.2的插件目录结构和文件名:
![](https://box.kancloud.cn/2015-12-19_5674d7751592f.png)
5.0 的:
![](https://box.kancloud.cn/2015-12-19_5674d77523cc2.png)
就是插件目录小写了,后缀变了。目录名也变了,然后类名不变。
以前一个控制器是BookMark/BookMark 现在变成了 book_mark/BookMark!!!
然后咱找个地方定义插件目录常量 入口:
`define('ONETHINK_ADDON_PATH', './addons/');`
添加命名空间:
Think\Loader::addNamespace('addons', ONETHINK_ADDON_PATH);
公共函数里添加命名空间印射目录。
然后修改common/behavior/init_hook.php 初始化钩子行为
~~~
<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006-2013 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------
namespace common\behavior;
use think\Hook;
use think\Loader;
defined('THINK_PATH') or exit();
// 初始化钩子信息
class InitHook{
// 行为扩展的执行入口必须是run
public function run(&$content) {
if (isset($_GET['m']) && $_GET['m'] === 'Install')
return;
// $data = null;
$data = S('hooks');
if (!$data) {
$hooks = D('Hooks')->getField('name,addons');
foreach ($hooks as $hook => $value) {
if ($value) {
$map['status'] = 1;
$names = explode(',', $value);
$map['name'] = array('IN', $names);
$data = D('Addons')->where($map)->getField('id,name');
if ($data) {
$addons = array_intersect($names, $data);
foreach ($addons as $key => $value) {
$dir = Loader::parseName(lcfirst($value));
$path = "./addons/{$dir}/".Loader::parseName(lcfirst($value)).".php";
$value = "addons\\{$dir}\\{$value}";
$path = realpath($path);
$addons[$key] = $value;
Loader::addMap($value, $path);
}
Hook::add($hook, $addons);
}
}
}
S('hooks', Hook::get());
} else {
Hook::import($data, false);
}
}
}
~~~
里面Loader::addMap 可是我实践出来的,开始以为定义了命名空间能自已找到类,结果得添加alias。
然后就是公共tags里添加行为:
~~~
<?php
return [
'app_init' => ['common\\behavior\\InitHook'],
];
~~~
这里写的都是命名空间规则,而不是路径。
common/controller/addon.php 插件定义抽象类修改,改getName 获取路径等方法,改着改着就成了这样:
~~~
<?php
namespace common\controller;
/**
* 插件类
* @author yangweijie <yangweijiester@gmail.com>
*/
abstract class Addon extends \think\controller{
/**
* 视图实例对象
* @var view
* @access protected
*/
protected $view = null;
/**
* $info = array(
* 'name'=>'Editor',
* 'title'=>'编辑器',
* 'description'=>'用于增强整站长文本的输入和显示',
* 'status'=>1,
* 'author'=>'thinkphp',
* 'version'=>'0.1'
* )
*/
public $info = array();
public $addon_path = '';
public $config_file = '';
public $custom_config = '';
public $admin_list = array();
public $custom_adminlist = '';
public $access_url = array();
public function __construct() {
$this->addon_path = ONETHINK_ADDON_PATH . lcfirst($this->getName()) . '/';
$parse_str = C('parse_str');
$parse_str['__ADDONROOT__'] = __ROOT__ . '/addons/' . $this->getName();
C('parse_str', $parse_str);
if (is_file($this->addon_path . 'config.php')) {
$this->config_file = $this->addon_path . 'config.php';
}
}
/**
* 模板主题设置
* @access protected
* @param string $theme 模版主题
* @return Action
*/
final protected function theme($theme) {
$this->view->theme($theme);
return $this;
}
final protected function addon_path(){
return ONETHINK_ADDON_PATH . lcfirst($this->getName()) . '/';
}
//显示方法
final protected function display($template = '') {
if (!is_file($template)) {
$template = $this->addon_path() . $template . '.html';
if (!is_file($template)) {
throw new \Exception("模板不存在:$template");
}
}
$html = $this->fetch($template);
return $html;
}
/**
* 获取插件目录或名称
* @param $type 0 - dir 1- name
*/
final public function getName($type = 0) {
$class = get_class($this);
$class = str_replace("addons\\", '', $class);
$class_arr = explode('\\', $class);
return $class_arr[$type];
}
final public function checkInfo() {
$info_check_keys = array('name', 'title', 'description', 'status', 'author', 'version');
foreach ($info_check_keys as $value) {
if (!array_key_exists($value, $this->info))
return FALSE;
}
return TRUE;
}
/**
* 获取插件的配置数组
*/
final public function getConfig($name = '') {
static $_config = array();
if (empty($name)) {
$name = $this->getName(1);
}
if (isset($_config[$name])) {
return $_config[$name];
}
$config = array();
$map['name'] = $name;
$map['status'] = 1;
$config = D('Addons')->where($map)->getField('config');
if ($config) {
$config = json_decode($config, true);
return $config;
} else if(!empty($this->config_file)){
$temp_arr = include $this->config_file;
foreach ($temp_arr as $key => $value) {
if ($value['type'] == 'group') {
foreach ($value['options'] as $gkey => $gvalue) {
foreach ($gvalue['options'] as $ikey => $ivalue) {
$config[$ikey] = $ivalue['value'];
}
}
} else {
$config[$key] = $temp_arr[$key]['value'];
}
}
$_config[$name] = $config;
return $config;
}else{
return [];
}
}
//必须实现安装
abstract public function install();
//必须卸载插件方法
abstract public function uninstall();
}
~~~
后来就是改后台相关的类。主要改了显示的地方,config方法和controller的冲突了换成conf。后面框架会去掉这个方法。
home 和admin 插件控制器执行方法:
~~~
<?php
namespace home\controller;
class Addons extends \think\controller{
protected $addons = null;
public function execute($_addons = null, $_controller = null, $_action = null){
$dir = \think\Loader::parseName($_addons, 0);
$_controller = \think\Loader::parseName($_controller,1);
$parse_str = C('parse_str');
$parse_str['__ADDONROOT__'] = __ROOT__ . "/addons/{$dir}";
C('parse_str', $parse_str);
if(!empty($_addons) && !empty($_controller) && !empty($_action)){
$addons = A("addons\\$dir/$dir")->$_action();
return $addons;
} else {
return $this->error('没有指定插件名称,控制器或操作!');
}
}
public function display($tpl = ''){
if (!is_file($tpl)) {
$tpl = $this->addon_path() . $tpl . '.html';
if (!is_file($tpl)) {
throw new \Exception("模板不存在:$tpl");
}
}
$html = $this->fetch($tpl);
return $html;
}
public function success($msg = '', $data = '', $url = '', $wait = 3){
\think\Response::success($msg, $data, $url, $wait);
}
public function error($msg = '', $data = '', $url = '', $wait = 3){
\think\Response::error($msg, $data, $url, $wait);
}
}
~~~
PS:后台插件快速创建的还么改,大家可以自己参考类修改一下,因为这只是老杨自己的移植,并不是官方tp5 以后的插件架构。
值得注意的是插件模板的显示输出不要exit 也不要return 回给Hook::listen 里,因为, 一个钩子有多个插件,插件执行不一定都是返回模板。
所以,如果你要显示什么 请 @print($this->display('single')) 这样去写。
## SAE上如何使用
think/model/sae 里的配置,要加入convetion.php里的部分配置。
如果你只是但应用,数据库就在当前应用,不需要建议database.php去配置数据库,如果是跨应用的,就要将来源引用的配置 常量 dump出来写死在项目公共数据库配置里。
然后是初始化一些服务 用于缓存的 memcache服务,用于文件上传的storage、注意sae的特性,不可写。
配置重定向要改 config.yaml
其他基本上没遇到问题。
# 尝试一下吧~
更多的以最新框架源码为准,只要没发布,每天更新仓库后自己手动覆盖已经保存svn或git的本地库,看看变化,自己的hack是否要去除了。
下载地址<http://git.oschina.net/yangweijie/oneblog/tree/5.0beta/> 自己克隆了 切换分支后看readme。
尝鲜就得会折腾。