扩展机制
突然想起一个主题,如何适应企业级开发,扩展机制应该也是其中的范畴。
关于扩展机制,禅道官方的说法:
易软天创团队使用PHP这十几年过程中,也曾经使用过很多PHP开源的软件。
在使用过程中,遇到了一个同样的问题:如果对代码做过个性化的修改,就没有办法跟着官方的版本进行升级了。
做得稍好一些的比如wordpress, dupral, discuz这些程序后来有了自己的hook扩展机制。但这种扩展机制是基于动作或者事件的,只能对原有的系统做局部的修改,限制性比较强,没有办法对系统做比较深入的修改。
带着这个问题,我们在设计zentaoPHP框架的时候,就特别注意框架的扩展性。得益于PHP5.2版本以后oop语法的增强,zentaoPHP框架实现了深入彻底的扩展机制。
其中提到的『基于动作或者事件的,只能对系统局部修改』即是他们做出选择和取舍的关键原因,我们也可以想像出来,面对国内用户群体的,那种不讲逻辑、不考虑整体性和长期维护的无奈吧。
个人看法是,系统的产品成熟度还有所不够,才会面临这个的问题。产品的设计,还得站在更高的维度去思考,产品架构决定了支持的业务模式范围。比如,禅道的工作流机制,这两三年的版本,逐步出来一个雏形,但是是硬编码的,而不是通过引入一套完整的工作流引擎来解决问题,那这样就必定不能让普通用户的层面去解决实际工作场景的需求,而需要程序员出马。
同时,一种设计可能只能解决所针对的相关问题,按上述官方说法中能解决跟随官方版本升级的问题,我看市未必的。这其中有更多因素要考虑,比如系统API的稳定性,第三方开发者生态建设等等。从代码的实际变动情况来看,官方团队并没有考虑太多,而且由于代码长期处于快速重构中,API的兼容性就完全没有体现,各种类、属性、方法在小版本甚至修订版本间就不兼容,对第三方开发者不友好。
当然,这里并没有批评的意思,只是就事论事谈一点感受。作为程序员,个人对易软天创这样的团队非常敬佩,坚持开源精神,同时业务能力过硬,生存能力又很强,我自己是远远不及。
问题建议
这么多年陆陆续续为所在团队写过一些禅道的扩展,但是当想要将这些扩展开源出去的时候,就会遇到几个很难受的问题;
- 在开发阶段,单个扩展的相应代码不能单独建立代码仓库进行管理;
- API不稳定,变化太快,兼容性差;
- 对开发工具不友好,代码得不到提示和导航跳转等;
打包好的扩展代码解压覆盖到系统的目录这个体验是ok的,但是开发扩展时,就不能为单个扩展建立一个Git代码仓库进行开发管理,这个对于现代人来说,太没有安全感了。
API稳定性,这个需要官方开发团队站在生态的角度看,同时也可以反过来要求官方团队成员更注重代码质量,因为一旦发布出来版本代码就不能随意更改。
现代开发工具VSCode也好、PhpStorm也好,智能度比较高,如果能好好利用,是可以大大提高开发效率的。这方面有几条简单的建议:
- 框架层代码强化类型注解;
- 业务层代码强化动态属性注解;
- 利用好PHP的类自动加载机制,减少动态对象创建
- 配置项Spec化,少用stdClass方式直接创建对象
框架层几个类,各属性的类型、方法的参数与返回值类型,加上准确的类型注解,如baseRouter类, $control、$config、$lang、$dbh、$post、$get等属性,createApp、loadCommon等方法的返回值,等等。
class baseRouter
{
/**
* $session对象,用于访问$_SESSION变量。
* The $session object, used to access the $_SESSION var.
*
* @var super
* @access public
*/
public $session;
/**
* 创建一个应用。
* Create an application.
*
* @param string $appName 应用名称。 The name of the app.
* @param string $appRoot 应用根路径。The root path of the app.
* @param string $className 应用类名,如果对router类做了扩展,需要指定类名。When extends router class, you should pass in the child router class name.
* @static
* @access public
* @return
outer the app object
*/
public static function createApp($appName = 'demo', $appRoot = '', $className = '')
{
if(empty($className)) $className = __CLASS__;
return new $className($appName, $appRoot);
}
/**
* Class router
* @property userModel $user 用户对象
* @property companyModel $company 当前公司信息
*/
class router extends baseRouter
{
class baseHelper
{
/**
* Registers an SPL autoloader.
*/
public static function registerAutoload()
{
ini_set('unserialize_callback_func', 'spl_autoload_call');
spl_autoload_register(array(static::class, 'autoloadModelClass'));
}
/**
* Handles autoloading of classes.
*
* @param string $class - A class name.
* @return boolean - Returns true if the class has been loaded
*/
public static function autoloadModelClass($class)
{
global $app;
$suffix = 'Model';
if ($suffix !== substr($class, - strlen($suffix))) {
return;
}
$realName = substr($class, 0, strlen($class) - strlen($suffix));
$modelFile = $app->setModelFile($realName);
return static::import($modelFile);
}
class baseModel
{
/**
* 创建当前模型的实例,但是得兼容ZenTao的模型扩展机制
* @return $this
*/
public static function instance()
{
global $common; /** @var commonModel $common */
// 去除后缀名 "Model", 仅在未启用命名空间的前提下成立
$className = static::class;
$realName = substr($className, 0, strlen($className) - strlen('Model'));
$model = $common->loadModel($realName);
return $model;
}
class my extends control
{
public function todo($type = 'all', $account = '', $status = 'all', $orderBy = "date_desc,status,begin", $recTotal = 0, $recPerPage = 20, $pageID = 1)
{
.....
// $this->loadModel('todo') 改为 todoModel::instance()
$this->view->todos = todoModel::instance()->getList($type, $account, $status, 0, $pager, $sort);
扩展管理的优化方案
针对上述三个问题中的第一条,做了点实战,发现经过极少量的代码就实现了,所以贴出来分享一下。
扩展管理,是个独立的话题,按照常规经验,最好是放在独立的目录,基于独立标识进行区分,这方面的设计在大多数系统中都可以看到。
比如,这里给出的方案是这样:
项目根目录/
ext/
easycorp-xuanxuan/
action/
admin/
block/
...
setting/
easycorp-xuanxuan
是扩展的标识,由两部分构成:团队标识、扩展名称,中间以『-』连接。
查看代码,找到了一个统一构造扩展查找路径的方法,赞,封装得不错,所以只要加几句话就可以了。
// baseRouter.class.php
/**
* 获取一个模块的扩展路径。 Get extension path of one module.
*
* If the extensionLevel == 0, return empty array.
* If the extensionLevel == 1, return the common extension directory.
* If the extensionLevel == 2, return the common and site extension directories.
*
* @param string $appName the app name
* @param string $moduleName the module name
* @param string $ext the extension type, can be control|model|view|lang|config
* @access public
* @return string the extension path.
*/
public function getModuleExtPath($appName, $moduleName, $ext)
{
/* 检查失败或者extensionLevel为0,直接返回空。If check failed or extensionLevel == 0, return empty array. */
if(!$this->checkModuleName($moduleName) or $this->config->framework->extensionLevel == 0) return array();
/* When extensionLevel == 1. */
$modulePath = $this->getModulePath($appName, $moduleName);
$paths = array();
$paths['common'] = $modulePath . 'ext' . DS . $ext . DS;
// 增加的逻辑 在新的扩展管理结构下寻找
$extDirs = helper::ls($this->getBasePath() . 'ext');
if (!empty($extDirs)) {
foreach ($extDirs as $extDir) {
if (empty($extDir) || !is_dir($extDir)) continue;
$id = basename($extDir);
$paths[$id] = $extDir . DS . $moduleName . DS . $ext . DS;
}
}
// 增加的逻辑结束
if($this->config->framework->extensionLevel == 1) return $paths;
/* When extensionLevel == 2. */
$paths['site'] = empty($this->siteCode) ? '' : $modulePath . 'ext' . DS . '_' . $this->siteCode . DS . $ext . DS;
return $paths;
}
默认代码中『xuanxuan』相关功能是以扩展的形式实现的,将原来分散在module/目录下各个模块中的ext代码移至到系统根目录下的ext/easycorp-xuanxuan目录下,继续按模块摆放。基于 ZenTaoPMS 12.5 stable 版本代码进行调试。测试了一下,基本正常。