x3d

扩展机制

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

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

易软天创团队使用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 \router  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 版本代码进行调试。测试了一下,基本正常。

分类:

技术点:

相关文章: