• 禅道项目管理系统ZenTaoPMS扩展机制之扩展管理优化方案


    扩展机制

    突然想起一个主题,如何适应企业级开发,扩展机制应该也是其中的范畴。

    关于扩展机制,禅道官方的说法:

    易软天创团队使用PHP这十几年过程中,也曾经使用过很多PHP开源的软件。
    
    在使用过程中,遇到了一个同样的问题:如果对代码做过个性化的修改,就没有办法跟着官方的版本进行升级了。
    
    做得稍好一些的比如wordpress, dupral, discuz这些程序后来有了自己的hook扩展机制。但这种扩展机制是基于动作或者事件的,只能对原有的系统做局部的修改,限制性比较强,没有办法对系统做比较深入的修改。
    
    带着这个问题,我们在设计zentaoPHP框架的时候,就特别注意框架的扩展性。得益于PHP5.2版本以后oop语法的增强,zentaoPHP框架实现了深入彻底的扩展机制。
    

    其中提到的『基于动作或者事件的,只能对系统局部修改』即是他们做出选择和取舍的关键原因,我们也可以想像出来,面对国内用户群体的,那种不讲逻辑、不考虑整体性和长期维护的无奈吧。

    个人看法是,系统的产品成熟度还有所不够,才会面临这个的问题。产品的设计,还得站在更高的维度去思考,产品架构决定了支持的业务模式范围。比如,禅道的工作流机制,这两三年的版本,逐步出来一个雏形,但是是硬编码的,而不是通过引入一套完整的工作流引擎来解决问题,那这样就必定不能让普通用户的层面去解决实际工作场景的需求,而需要程序员出马。

    同时,一种设计可能只能解决所针对的相关问题,按上述官方说法中能解决跟随官方版本升级的问题,我看市未必的。这其中有更多因素要考虑,比如系统API的稳定性,第三方开发者生态建设等等。从代码的实际变动情况来看,官方团队并没有考虑太多,而且由于代码长期处于快速重构中,API的兼容性就完全没有体现,各种类、属性、方法在小版本甚至修订版本间就不兼容,对第三方开发者不友好。

    当然,这里并没有批评的意思,只是就事论事谈一点感受。作为程序员,个人对易软天创这样的团队非常敬佩,坚持开源精神,同时业务能力过硬,生存能力又很强,我自己是远远不及。

    问题建议

    这么多年陆陆续续为所在团队写过一些禅道的扩展,但是当想要将这些扩展开源出去的时候,就会遇到几个很难受的问题;

    1. 在开发阶段,单个扩展的相应代码不能单独建立代码仓库进行管理;
    2. API不稳定,变化太快,兼容性差;
    3. 对开发工具不友好,代码得不到提示和导航跳转等;

    打包好的扩展代码解压覆盖到系统的目录这个体验是ok的,但是开发扩展时,就不能为单个扩展建立一个Git代码仓库进行开发管理,这个对于现代人来说,太没有安全感了。

    API稳定性,这个需要官方开发团队站在生态的角度看,同时也可以反过来要求官方团队成员更注重代码质量,因为一旦发布出来版本代码就不能随意更改。

    现代开发工具VSCode也好、PhpStorm也好,智能度比较高,如果能好好利用,是可以大大提高开发效率的。这方面有几条简单的建议:

    1. 框架层代码强化类型注解;
    2. 业务层代码强化动态属性注解;
    3. 利用好PHP的类自动加载机制,减少动态对象创建
    4. 配置项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 版本代码进行调试。测试了一下,基本正常。

  • 相关阅读:
    ibatis集成封装之路(to mysql)
    设计模式第一弹—适配器模式
    markdown语法
    outlook vba开发要点
    PHP中json_encode中文编码的问题_学习
    isset、empty、var==null、is_null、var===null详细理解
    对冒泡和二分法,特别是二分法有了更深的理解
    php Socket表单提交学习一下
    php Socket模拟表单上传文件函数_学习
    java第九节 网络编程的基础知识
  • 原文地址:https://www.cnblogs.com/x3d/p/zentao-extension-management.html
Copyright © 2020-2023  润新知