AlloVince is Evil

EVAEngine

Yet another development engine based on Phalcon

1.1 Why Phalcon

根据业务做技术选型,我们的需求是什么

  1. 产品形态非常复杂(新闻、行情、社区)
  2. 必须快
  3. 松耦合,每个人能独立承担业务

  1. 小框架还是大框架
  2. 有没有可能组合两个框架
  3. 对于PHP重框架有什么优化手段

1.2.1 段子

  • ZF2:我有官方支持
  • Phalcon:我快
  • Symfony:我有数不清的Bundle
  • Phalcon:我快
  • Laravel:我代码书写无比优雅
  • Phalcon:我快

天下武功、唯快不破

1.2.2 优缺点

优点

  • 性能提升6~15倍
  • 框架功能够用,可以解决70%的需求
  • DI设计优秀,扩展性好

缺点

  • 黑盒
  • 辅助工具简陋
  • 部署运维及测试略麻烦
  • 一些小坑

1.3 Phalcon存在的问题及坑

  1. 默认启动的DI太简单,不能满足大型项目需求
  2. 模块实现较弱,模块加载在路由之后
  3. View不够友好,强制关联Layout和Template,没有考虑多模块
  4. Form太简陋,没有考虑数据关联的情况
  5. 一些奇怪的行为(没有View时不抛异常、Dispatch抛异常得到白屏等)

2.1 EvaEngine所要实现的

修改原则:避免深度定制,避免覆盖接口。通过扩展新方法及调用事件为主

  1. 内置配置好的DI,将系统逻辑简化到配置文件级别 √
  2. 真正意义上的模块化 √
  3. View的扩展,支持模块间调用 √
  4. Form的扩展,Form间可以关联 √
  5. 统一异常处理封装 √
  6. 模块间的协同处理(考虑插件?)
  7. 方便的数据库迁移工具(含ORM映射)
  8. 更好的调试工具(Router/Events)

目标:所有资源集中到一个框架,通过组合模块完成80%的基础工作

2.2 EvaEngine的层级划分

- Application (Web) 每个Application都有唯一的Name
  - Application Config
    - Module Load Config 命名根据Application Name
    - Global Config
    - Local Config
  - Module
    - Module Bootstrap File
    - Module Config
    - Controller
      - Admin Controller
    - Model
      - Entity
    - Form
    - Events Listener 
    - View
      - Helper
      - Partial
      - Admin View
  - Another Module
- Another Application (API)
                    

2.3 EvaEngine的目录结构

- apps #存放Application
- cache #数据/页面缓存
- config #全局配置/配置文件缓存
- modules #存放公共模块
  - EvaUser #模块
    - Module.php #模块启动文件
    - config #模块配置/前台路由/后台路由
    - views  #模块Views
      - layouts #布局
      - index/index #模板
      - partial #组件
      - admin #后台
        - layouts #布局
        - partial #组件
    - src #源代码
      - EvaUser #PSR-2
        - Controllers
          - Admin #后台
        - Models
        - Entities
        - Forms
        - Events
    - Utils
        - View
          - Helpers
- public #入口文件夹
- utilities #辅助工具
- workers #后台任务

2.4 EvaEngine的启动代码

use Eva\EvaEngine\Engine;

//设置Application根目录, 设置AppName
$engine = new Engine(__DIR__ . '/..', 'AppName'); 

$engine
->loadModules(include __DIR__ . '/../config/modules.' . $engine->getAppName() . '.php') 
//根据AppName加载模块配置文件,初始化DI
->bootstrap() //初始化Application,注册模块事件,注册ErrorHandler
->run(); //Phalcon\Application->handle()

3.1 EvaEngine的Module

如何定义模块

  1. 具有独立的一组功能,可以支持一个最小规模的产品(低耦合,高内聚)
  2. 有自己的MVC
  3. 有自己的管理后台
  4. 有自己的前台路由(可选)
  5. 有自己的后台路由(可选)
  6. 模块启动文件必须实现接口Eva\EvaEngine\Module\StandardInterface

3.2 Module的加载过程

  1. 通过DI实例化EvaEngine\Module\ModuleManager
  2. 设置Module默认路径$moduleManager->setDefaultPath()
  3. 正式加载$moduleManager->setDefaultPath()
    1. 触发事件module:beforeLoadModule
    2. 配置模块,注册模块启动文件
    3. 注册全局加载器Module::registerGlobalAutoloaders()[缓存]
    4. 注册全局事件Module::registerGlobalEventListeners()[缓存]
    5. 注册全局HelperModule::registerGlobalViewHelpers()[缓存]
    6. 注册跨模块的Entity关系Module::registerGlobalRelations()[缓存]
    7. 触发事件module:afterLoadModule
  4. Phalcon原始模块启动过程

3.3 如何配置一个模块

默认配置

$engine->loadModules(array("EvaUser"));

自定义配置

$engine->loadModules(array(
"EvaUser" => array( //模块名
    'className' => 'Eva\EvaUser\Module', //模块启动文件Class名
    'path' => __DIR__ . '/../modules/EvaUser/Module.php', //模块启动文件路径
    'moduleConfig' => '/../modules/EvaUser/config/config.php', //模块配置文件路径
    'routesFrontend' =>  '/../modules/EvaUser/config/routes.frontend.php', //模块前台路由配置文件路径
    'routesBackend' =>  '/../modules/EvaUser/config/routes.backend.php', //模块后台路由配置文件路径
    'adminMenu' => '', //模块后台导航文件
)));

4.1 默认配置的DI,其一

  • ModuleManager : 模块管理器 (EvaEngine实现)
  • EventsManager : 事件管理器(已启用优先级)
  • Config : 全局配置
  • Router : 路由
  • Dispatcher : 分发器(事件已注入)
  • ModelsMetadata : 表结构(默认开启文件缓存)
  • ModelsManager: Model管理器(默认开启主从)
  • View : View (EvaEngine\View)
  • Session : Session(默认使用文件)
  • DbMaster / DbSlave : 数据库主从
  • ViewCache / ModelCache / ApiCache: 默认均为文件
  • Queue : 队列,默认为GearmanClient
  • Worker : 默认为GearmanWorker
  • Mailer : 邮件发送服务,默认为Swift_Mailer
  • MailMessage: 邮件主题,支持模板,EvaEngine\MailMessage

4.2 默认配置的DI,其二

  • Url : Url生成器
  • Escaper : 转义
  • Tag : View标签 EvaEngine\Tag
  • Flash : 提示信息处理
  • Placeholder : 模板占位符
  • Cookies : 默认关闭加密
  • Translate : I18N实现,默认使用csv
  • FileSystem : 文件服务,基于Gaufrette

启动DI的推荐方法:

$this->getDI()->getModuleManager();

不推荐:

$this->getDI()->get('moduleManager');

5.1 异常处理

什么时候用异常:当主要业务无法继续进行(注册、登录、访问资源不存在)

什么时候不用异常:伴随主业务的附加服务(注册发邮件、用户得积分)

  • 所有异常会抛至顶层,由Eva\EvaEngine\ErrorHandler统一接管
  • ErrorHandler可替换
  • Debug开启时,ErrorHandler为Phalcon\Debug
  • ErrorHandler根据Exception不同,产生不同响应(Http Status Code / HTML / JSON)
  • EvaEngine的异常必须实现接口Eva\EvaEngine\Exception\ExceptionInterface。异常中携带了Http Status Code

5.2 异常的归类

- StandardException                      #标准异常     500
  - LogicException                       #逻辑异常     500
    - BadFunctionCallException           #错误函数调用 400
    - BadMethodCallException             #错误方法调用 405
    - DomainException                    #Domain错误   400 
    - InvalidArgumentException           #错误的参数   400 
    - LengthException                    #长度错误     400 
    - OutOfRangeException                #超出范围     400
    - OperationNotPermitedException      #操作不允许   403 
    - ResourceConflictException          #资源冲突     409 
    - ResourceExpiredException           #资源过期     403 
    - ResourceNotFoundException          #资源不存在   404
    - UnauthorizedException              #资源未授权   401
    - VerifyFailedException              #验证失败     403
  - RuntimeException                     #运行异常     500
    - IOException                        #IO错误       500 
                    

5.3 异常举例

  • 用户名重复 ResourceConflictException 409
  • 输入API参数错误 InvalidArgumentException 400
  • 访问不存在的文章 ResourceNotFoundException 404
  • POST接口使用GET访问 BadMethodCallException 405
  • 未登录访问后台 UnauthorizedException 401
  • 修改无权修改的资源 OperationNotPermitedException 403
  • 文件无法写入 IOException 500
  • 核心错误 RuntimeException 500

错误代码规范: ERR_模块_异常信息

throw new Exception\UnauthorizedException('ERR_USER_NOT_ACTIVATED');

5.4 内部错误代码

待定

6.1 对Controller的扩展

通过不同的接口实现来区分数据类型:HTML/JSON,权限:Session/Token

  • JsonControllerInterface 数据自动支持Json/Jsonp
  • SessionAuthorityControllerInterface 基于Session验证
  • TokenAuthorityControllerInterface 基于Token验证

7.1 对Model的扩展

  • 默认开启主从
  • 支持表前缀,默认为`eva_`
  • initialize()会默认加载跨模块的Entity关系
  • dump()用于将带有关系的Entity输出为数组

8.1 对View的扩展

基于模块名设置模版

$view->setModuleLayout('EvaCommon', '/views/admin/layouts/layout');

基于模块名设置View的根目录

$view->setModuleViewsDir('EvaBlog', '/views');

基于模块名设置区块的根目录

$view->setModulePartialsDir('EvaCommon', '/views');

9.1 对Form的扩展

Form可以绑定Model

protected $defaultModelClass = 'Eva\EvaBlog\Models\Text';

Form间可以关联

        $form = new Forms\PostForm();
        $post = new Models\Post();
        $form->setModel($post);
        $form->addForm('text', 'Eva\EvaBlog\Forms\TextForm');

关联后统一验证,验证完毕通过Form保存所有关联数据

        $data = $this->request->getPost();
        if (!$form->isFullValid($data)) {
            $form->save('updatePost');
        }

子表单的输出会自动添加前缀

$form->getForm('text')->render('content');
<input name="text[content]" />

可以通过注解及绑定Model自动初始化Form Elemens

    /**
     * @Type(Select)
     * @Option(deleted=Deleted)
     */
    public $status;

10.1 后台的UI控件

待定

11.1 未解决的问题

  • 如何应对字段的变更
  • 简单可行的CMS缓存策略
  • 多对多及复杂情况的Form处理