2014年11月

组件

组件

整个xts框架都由组件构成。xts提供的所有功能被封装在若干个组件对象里,组件可以通过配置绑定在XComponentFactory类上以方便调用,XComponentFactory类还有一个缩写,就是X类。xts的组件需要实现xts\IComponent接口,这个接口的定义了xts组件必需是可配置的,而且要支持Getter和Setter的类。

interface IComponent {
    /**
     * @param array $conf
     * @return void
     */
    public static function conf($conf=array());

    /**
     * @param string $name
     * @return mixed
     */
    public function __get($name);

    /**
     * @param string $name
     * @param mixed $value
     * @return void
     */
    public function __set($name, $value);

    /**
     * @param string $name
     * @return bool
     */
    public function __isset($name);

    /**
     * @return array
     */
    public function getConf();
}

xts本身已经提供了一批组件,涵盖小型项目需要的各项功能,包括:

  • apple——应用路由器
  • cache——内存缓存(支持使用memcache或者redis)
  • cc——文件缓存(本机缓存)
  • db——MySQL数据库查询器
  • orange——对象关系映射(ORM)类
  • view——RainTPL模板引擎
  • valley——表单数据验证器
  • redis——PhpRedis的xts封装
  • smarty——Smarty模板引擎的xts包装(需要用户自己下载smarty程序)

自定义组件

用户也可能开发自己的组件,或者把第三方库包装成xts组件。

开发自己的组件比较简单,可以直接继承xts\Component类,在这个类中已经实现了IComponent接口,完成了基本的Getter和Setter支持。如果要把第三方的库包装成xts组件,就需要用户自己实现IComponent接口。实现IComponent接口时可以参考xts\Component类中的代码,xts中类的属性Getter的命名格式应该是getFullName这样的驼峰命名。未来xts考虑提供一个trait来实现这个接口的基本功能。

组件工厂

XComponentFactory类能根据配置生成组件对象实例。它支持单例模式,配置单例模式后生成的实例会在组件工厂内保留一份引用,下次请求时会直接return,不会再重新new创建实例。下面的配置定义了一个view组件:

  1. 它是xts\Hail类的实例
  2. 初始化以前应该先require {{X_LIB_ROOT}}/hail.php文件(X_LIB_ROOT是xts框架的库目录路径常量)
  3. 它要求启用单例支持

    array( 'component' => array( 'view' => array( 'class' => '\xts\Hail', 'require' => X_LIB_ROOT.'/hail.php', 'singleton' => true, 'conf' => array( 'tpl_dir' => X_PROJECT_ROOT.'/protected/view', 'tpl_ext' => 'html', 'compile_dir' => X_RUNTIME_ROOT.'/compiled_template', 'enable_clip' => false, 'cacheId' => 'cc', // string to cache component id or false to disable cache 'cacheDuration' => 60, // page cache duration, second ) ), ), ),

在前面马上开始的例子里X::view()会触发xts\XComponentFactory类中的__callStatic魔术方法,这个魔术方法会取回配置文件里定义的组件ID为view的组件,在实例化之前,配置中conf数组会传递给组件类的conf静态方法。在前面xts配置的例子里,配置文件定义了apple、db、view三个组件,就可以用X::apple()X::db()X::view()的方式直接访问。

使用XComponentFactory初始化组件的时候,参数会传递给组件类的构造函数。比如下面的组件配置声明了一个不使用单件的orange组件,返回xts\Orange类的实例:

array(
    'component' => array(
        'orange' => array(
            'class' => '\\xts\\Orange',
            'singleton' => false,
            'conf' => array(
                'queryId' => 'db',
                'enableCacheByDefault' => false,
            ),
        ),
    ),
),

调用X::orange('user')的时候,组件工厂会返回相当于new Orange('user')的执行结果。

xts配置

Object Based Program

PHP最早和C语言一样,走面向过程的道路。作为一个典型的CGI应用,PHP程序从Web服务器请求.php文件开始,到返回页面HTML结束。开发PHP程序的主要任务有两种:验证数据,写入数据库;准备数据,组织页面。不论哪种程序的编写,用面向过程的方式都会非常直观和清晰。

Web项目越发复杂后,PHP从5.0开始提供比较强大的OOP机制,也出现了一批OOP的开发框架。但是很多PHP程序员都深刻感觉到,想跟踪框架的代码运行过程非常绕,非常困难。这是因为OOP开发更注重类和对象的概念,认为编程就是对象间彼此通信,发送消息,所以具体的代码被割裂到许多类的许多方法中。

OOP也有好处,它提供了一个比关联数组更严谨的操作机制。比如我们可以通过编写一个Getter来格式化数据库里取出的某个字段,甚至可以创造一个DataBase里没有字段在运行时使用。虽然关联数组也可以随时创建一个key保存任意的value,但分散于代码各处的写入和读取操作会给软件维护带来很大的麻烦。有一种观点是,面向对象机制其实给开发人员提供了一种逻辑上更贴近自然的代码分类、组织和管理的方式。

xts提倡基于对象开发的模式。保留PHP既有的过程化开发,同时对于一些可以内敛的地方用对象加以包装,提升代码复用率。xts不建议开发人员修改框架中的代码,可以直接简单的使用之。

覆盖式配置

在项目的protected/config目录里有两个配置文件,一个人是debug.php,另一个是release.php。xts默认使用debug.php中的配置。当运行build脚本时,xts会把webroot/xts.php文件中定义的X_DEBUG常量改为false,此时xts会使用release.php中的配置覆盖debug.php中的同名配置。

覆盖式配置的好处是相同的配置项无需写两遍,只需要在release.php中写上不同的配置项即可。我曾不止一次遇到需求要改配置即可完成,在debug配置里改了,release配置里忘了改,测试没有任何问题,上线就出bug。xts的等位覆盖机制可以完美避免此类问题。

下面是一个debug配置的例子:

<?php
return array(
    'component' => array(
        'apple' => array(
            'class' => '\\xts\\Apple',
            'singleton' => true,
            'conf' => array(
                'actionDir' => X_PROJECT_ROOT.'/protected/action',
                'defaultAction' => '/index',
                'actionPrefix' => 'action_',
                'preAction' => '',
                'preActionFile' => '',
            ),
        ),
        'db' => array(
            'class' => '\\xts\\Query',
            'singleton' => true,
            'conf' => array(
                'host' => 'localhost',
                'port' => 3306,
                'schema' => 'xts',
                'charset' => 'utf8',
                'user' => 'xts',
                'password' => 'xtstest',
                'persistent' => false,
            ),
        ),
        'view' => array(
            'class' => '\\xts\\Hail',
            'require' => X_LIB_ROOT.'/hail.php',
            'singleton' => true,
            'conf' => array(
                'tpl_dir' => X_PROJECT_ROOT.'/protected/view',
                'tpl_ext' => 'html',
                'compile_dir' => X_RUNTIME_ROOT.'/compiled_template',
                'enable_clip' => false,
                'cacheId' => 'cc', // string to cache component id or false to disable cache
                'cacheDuration' => 60, // page cache duration, second
            )
        ),
    ),
);

在release配置中,由于只有数据库的库名、登录密码、长连接的配置有所不同,所以可以只写这些:

<?php
return array(
    'component' => array(
        'db' => array(
            'conf' => array(
                'schema' => 'xts_example',
                'password' => 'dk23Jssk887^0',
                'persistent' => true,
            ),
        ),
    ),
);

xts在X_DEBUG置为false时,用release配置覆盖debug配置,合并成实际使用的配置数组。

马上开始

从xts-init初始化

虽然xts最早实现的是ORM功能,个人感觉最有价值的也是这块,但是入门还是能马上动手开始实践比较好。xts-init项目提供了一个可以快速开始的项目文件目录模板。直接clone这个项目并重命名就可以了:

git clone git@github.com:SyuTingSong/xts-init.git MyProject

因为xts项目是以git子模块的方式存在于xts-init项目中,所以还需要用相应命令取出它的代码。

cd MyProject
git submodule init
git submodule update

接下来还需要清理一下xts-init项目原本的git信息:

rm -rf .git .gitmodules framework/.git

现在整个xts框架目录就已经准备好了。

目录结构说明

从xts-init初始化好的目录结构如下:

.
├── CREDITS
├── LICENSE
├── README.md
├── build
├── build.tools
│   ├── turn-off-debug.patch
│   └── yuicompressor-2.4.8.jar
├── framework
│   ├── CJSON.php
│   ├── CREDITS
│   ├── LICENSE
│   ├── README.md
│   ├── apple.php
│   ├── base.php
│   ├── cache.php
│   ├── hail.php
│   ├── orange.php
│   ├── redis.php
│   ├── smarty.php
│   ├── valley.php
│   ├── view.php
│   └── x.php
├── protected
│   ├── action
│   │   └── index.php
│   ├── common.php
│   ├── config
│   │   ├── debug.php
│   │   └── release.php
│   ├── runtime
│   └── view
│       ├── index.html
│       └── layout.html
├── static
│   ├── css
│   │   └── common.css
│   ├── img
│   └── js
└── webroot
    ├── index.php
    ├── static -> ../static/
    └── xts.php

第一级主要有framework、protected、static、webroot和build.tools五个目录。framework目录里存放的是xts框架的源代码;protected目录用于存放我们的自己的程序代码、页面模板以及配置文件等等;static目录里保存静态的资源文件,包括css、JavaScript、图片、Web字体等等;webroot目录是Web服务器的根目录,可以直接被浏览器访问到,一般只有一个入口文件;build.tools是xts框架的编译工具,里面放置js压缩器还有关闭debug模式的patch。

webroot里有一个指向static的软链接,这样浏览器可以直接访问到static目录里的资源。把static目录放在webroot之外是为了方便地把static目录单独打包发布到CDN上。xts的build脚本可以把静态资源文件引用(就是HTML里的link还有img的src等等)替换成独立的静态服务器域名或者CDN域名。

protected目录里一般会有action、config、runtime、model、view几个目录。action目录用于存在controller代码;config里存在配置文件,xts的配置文件也是php程序,是一个大关联数组;runtime目录里存在一些运行时生成的数据,包括文件缓存,编译出来的模板,运行日志等等;model目录在xts-init里不会创建,这个目录是存在自定义数据库ORM对象的;view目录里存放xts的模板,一般以.html为后缀。

Hello, world

前面介绍了一大通xts的目录结构,现在马上开始试试写代码运行吧。打开protected/action/index.php文件,可以看到里面已经有Hello, world的Controller代码了。

<?php
function index() {
    X::view()
        ->setPageTitle('Hello world')
        ->render('index', array(
        'to' => 'world',
    ), '1');
}

每个xts的action都是一个函数,在protected/action目录里,存放在与之同名的.php文件中。xts的路由规则是尽最大可能匹配目录和文件。比如请求/a/b/c/d就会先尝试加载protected/action/a/b/c/d.php文件,如果不存在,会再尝试protected/action/a/b/c.php,如果仍未找到,会尝试protected/action/a/b.php,最后到protected/action/a.php。相应的,xts也会尝试调用d()、c()、b()最后是a()。调用的函数名总是和文件名相同例外情况

大写X是xts框架的一个静态类,上面绑定了根据配置文件加载的工具类。X::view()会返回一个RainTPL模板引擎的实例。当然,这里使用的RainTPL已经被修改以适合xts的整结构。这个action会通知模板引擎,渲染index模板,并传递参数$to = world。

打开protected/view/layout.htmlprotected/view/index.html文件,可以看到模板的代码。layout模板里写的是各页面公共的html片段,一般包括页面的css引用,header、footer、nav bar等等。

<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no" />
    <style media="handheld" type="text/css"></style>
    <link rel="stylesheet" href="/static/css/common.css" />
    <title>{$_page_title|escape}</title>
    </head>
<body>
{include="$_content_template"}
</body>
</html>

index模板里写的是具体页面的内容,它会被嵌入到layout的{include="$_content_template"}位置。

<h2>Hello, {$to}</h2>

在index模板里,使用来自action函数中赋值的变量$to。

Web服务器的配置

xts是单入口的框架,需要在web服务器上配置rewrite把所有请求都rewrite给webroot/index.php脚本才能使用。以nginx为例,可以使用它的try_files。

try_files $uri $uri/ /index.php?$args;

总之需要确保站点访问所有基于xts的程序都交给index.php就好了。

推荐的IDE

推荐使用IDEA出口的PhpStorm来开发xts程序,能获得最好的自动完成提示、方法参数说明等。如果不想花钱,ActiveState出品的Komodo Edit也是一个不错的选择,虽然比PhpStorm要差一些。最好不要使用Vi之类的编辑器开发,因为xts框架的一个设计就是利用IDE的自动完成最高效地开发程序,用Vi效率降一半呀。

命名

xts框架在类的方法和属性中使用驼峰命名法,在数据库、函数、网页参数中使用帕斯卡命名法。

我为什么要写xts

在我写xts之前,做PHP开发一般会用两种方式: 1. 使用原生PHP函数和类库开发 2. 使用Yii框架开发

这两种方式各有各的好。原生函数和类一般在开发小项目时使用,可以飞快地上手开发,开发速度一般,运行效率非常高;使用Yii这种成熟的开发框架,大量的工作都不再需要自己进行,但Yii框架的程序复杂臃肿,运行效率不高。每次用了一段时间的Yii框架之后,再换回原生PHP,总觉得少了些什么,各种东西都要自己编写管理,感觉非常不方便。但是对于我经常做的十几到二十几个页面的小型项目来说,Yii框架太复杂了,实际只使用了Yii框架不到5%的代码,但是每次运行都要各种初始化,处理各种分支。

我希望有一个简单一些的开发框架,牺牲一些灵活性,保留框架开发中舒服的部分,于是我尝试了redbean。这是一个ORM库,可实际用过之后感觉它的性能一般,与我的开发理念完全不合。在对比了ThinkPHP、CI之后,决定还是自己写一个。

在我看来,一个微型的PHP框架里应该包括三样不可缺少的东西: * 路由器。路由器实现了单入口支持,这样更容易控制程序先完成一些公共的任务,比如载入配置,建立数据库连接等等。 * DB和ORM。个人很喜欢ORM的方便,但有时直接操作SQL的确更为简单。 * 配置分离。同时支持debug和release模式的不同配置,可以根据DEBUG状态访问不同的数据库等等。

另外四样功能视情况,有时会很方便: * Cache。虽然只是get/set/del操作,但是如果能包一层屏蔽掉apc、memcache、redis等等的差异还是很不错的选择。 * 模板引擎。很多人认为PHP本身就是模板引擎,这一点我也同意,不过我不喜欢在html代码中写if(is_array($itemList)) foreach($itemList as $item):这种代码,还是直接用模板引擎帮我把{foreach}翻译一下好了。 * 表单验证器。虽然isset、empty、is_numeric等等PHP函数已经能提供验证,不过感觉写着还是很不方便,如果能有一种定义式语法完成表单验证会更舒服。 * 日志功能。日志也是一个开发过程中很重要的方面,如果框架提供日志功能,总会比自己fopen来得方便。

总的来说,框架也是一种软件复用的方式,xts就是想提供一个尽可能小的轻量级工具,再平衡一下开发工作量和运行效率的天平。