存档在 ‘Web开发’ 分类

RESTful style url in CodeIgniter

2011年1月3日

我一直很喜欢 Rails 风格的 url 样式,现在我在使用 CodeIgniter 框架开发东西,也想使用 Rails 那样的 url 样式。为了避免再一次发明轮子,我网上搜了下,发现很多人也有这个需求,而且也有了好几个相应的实现()。但是,这些实现要么配置麻烦,要么代码量太多改动太大,和 CodeIgniter 的 Write Less, Do More 的口号有点不协调。

所以,我只好自己发明一个轮子啦 :-)

“发明”完毕后,和其他实现对比了下,我自认我的实现比现有的我所能找到的实现都更简洁、逻辑清晰、容易配置、好用。不是我自夸,我自己用着觉得确实好用,觉得是个好东西(媳妇是别人的好,孩子是自己的好?:-) ),所以发出来给大家分享分享,欢迎大家指教。

先看一下怎么使用这个轮子:

routes.php 配置文件后面添加以下内容:

// 在 application/config/routes.php 后面(或合适位置)加上这一行
map_resources('users');

///////////////////////////////////////////////////////////////////////////////
// 如果你不需要自定义额外的 url 规则,请无视下面的代码。。。
///////////////////////////////////////////////////////////////////////////////

// custom action handles GET request
map_resources('GET', 'users/(:id)/custom_action', 'users/custom_action/$1'); 

// some other custom action mappings...
// custom action handles PUT request
//map_resources('PUT', 'users/(:id)/custom_action', 'sync_sessions/custom_action/$1');

// custom action handles all requests (GET, POST, PUT, DELETE)
//map_resources('users/(:id)/custom_action', 'sync_sessions/custom_action/$1');

// ...

然后 controller 里就可以像 Rails 一样啦(除了 new, 因为 php 语言的限制,没办法 ):

<?php
class Users extends Controller
{

    function Users()
    {
        parent::Controller();   
    }

    // GET /users
    function index()
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users\n";
    }

    // GET /users/12345
    function show($id)
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/{$id}\n";
    }

    // GET /users/new
    // I have to use new_form as method name because "new" is a keyword in php
    function new_form()
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/new\n";
    }

    // GET /users/12345/edit
    function edit($id)
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/{$id}/edit\n";
    }

    // POST /users
    function create()
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users\n";
    }

    // PUT /users/12345
    function update($id)
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/{$id}\n";
    }

    // DELETE /users/12345
    function delete($id)
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/{$id}\n";
    }

    // GET /users/12345/custom_action
    function custom_action($id)
    {
        echo __METHOD__ . "<br>\n" . $_SERVER['REQUEST_METHOD'] . " /users/{$id}/custom_action\n";
    }
}

看到这里如果你感兴趣的话,可以继续往下看怎么实现了。其实非常简单,只有一个 MY_Router.php 文件,不到 100 行的代码(不计算空行和注释的话)。安装也很简单:只需要把这个 MY_Router.php 放到 application/libraries 目录下就万事大吉了。 代码逻辑我就不一一解释了,里面有注释,逻辑也简单,相信每个人都能看懂。

<?php
/**
 * RESTful-CodeIgniter
 *
 * A RESTful url router library for CodeIgniter
 *
 * @author      ZHENJiNG LiANG (liangzhenjing_N_O_S_P_A_M@gmail.com)
 * @license     BSD license
 * @link        http://liang.eu/web-dev/php/restful-style-url-in-codeigniter
 */

///////////////////////////////////////////////////////////////////////////////
// NOTE: 
// This Library added two macros:
// :id as [^/]+ in order to match non-numeric id field,
// :uuid to match UUID (aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee)
//
// :any and :num remain the same
// See code below for details
// You can change at will
///////////////////////////////////////////////////////////////////////////////

///////////////////////////////////////////////////////////////////////////
// map_resources()                              --> Return all routes
// map_resources($controller_name)              --> Map default restful routes
// map_resources($method, $pattern, $replace)   --> Add a custom route
///////////////////////////////////////////////////////////////////////////
function map_resources()
{
    static $routes = array();

    $arg_num = func_num_args();
    if(0 == $arg_num)
    {
        return $routes;
    }
    elseif(1 == $arg_num)
    {
        // map_resources($controller_name)   --> return all routes
        $controller = func_get_arg(0);

        // Generate RESTful url match patterns
        $routes['GET']["{$controller}"]             = "{$controller}/index";
        $routes['GET']["{$controller}/new"]         = "{$controller}/new_form";
        $routes['GET']["{$controller}/(:id)"]       = "{$controller}/show/$1";
        $routes['GET']["{$controller}/(:id)/edit"]  = "{$controller}/edit/$1";

        $routes['POST']["{$controller}"]            = "{$controller}/create";
        $routes['PUT' ]["{$controller}/(:id)"]      = "{$controller}/update/$1";
        $routes['DELETE']["{$controller}/(:id)"]    = "{$controller}/delete/$1";
    }
    elseif(2 == $arg_num)
    {
        // Custom url routing
        $args    = func_get_args();
        $pattern = $args[0];
        $replace = $args[1];

        $routes['GET'][$pattern]    = $replace;
        $routes['POST'][$pattern]   = $replace;
        $routes['PUT'][$pattern]    = $replace;
        $routes['POST'][$pattern]   = $replace;
    }
    elseif(3 == $arg_num)
    {
        // Custom url routing
        $args    = func_get_args();
        $method  = $args[0];
        $pattern = $args[1];
        $replace = $args[2];

        $routes[$method][$pattern] = $replace;
    }
}

///////////////////////////////////////////////////////////////////////////
// RESTful style url router
///////////////////////////////////////////////////////////////////////////
class MY_Router extends CI_Router
{
    function MY_Router()
    {
        // HACK: support overridding REQUEST_METHOD by posting a _method parameter
        if($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_POST['_method']))
        {
            $_SERVER['REQUEST_METHOD'] = strtoupper($_POST['_method']);
        }
        elseif(isset($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']))
        {
            $_SERVER['REQUEST_METHOD'] = strtoupper($_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE']);
        }

        parent::CI_Router();
    }

    // Overrides CI_Router::_parse_routes. 
    // RESTful style url matching
    function _parse_routes()
    {
        $rest_routes = map_resources();

        // we do this for performence
        if (empty($rest_routes) && count($this->routes) == 1)
        {
            $this->_set_request($this->uri->segments);
            return;
        }

        // also this...
        $uri = implode('/', $this->uri->segments);
        if (isset($this->routes[$uri]))
        {
            $this->_set_request(explode('/', $this->routes[$uri]));
            return;
        }

        // RESTful url matching...
        $request_method = $_SERVER['REQUEST_METHOD'];
        $routes = (isset($rest_routes[$request_method]) && $rest_routes[$request_method]) ? $rest_routes[$request_method] : array();
        foreach($routes as $pattern => $replace)
        {
            $pattern = str_replace(':id',   '[^/]+',  $pattern); // use this to match non-numeric id field
            $pattern = str_replace(':any',  '.+',     $pattern);
            $pattern = str_replace(':num',  '[0-9]+', $pattern);
            $pattern = str_replace(':uuid', '[a-zA-Z0-9]{8}(-[a-zA-Z0-9]{4}){3}-[a-zA-Z0-9]{12}', $pattern);

            // Does the RegEx match?
            if (preg_match("#^{$pattern}$#", $uri))
            {
                // Do we have a back-reference?
                if (strpos($replace, '$') !== FALSE && strpos($pattern, '(') !== FALSE)
                {
                    $replace = preg_replace("#^{$pattern}$#", $replace, $uri);
                }

                // we are done
                $this->_set_request(explode('/', $replace));
                return;
            }
        }

        // if non of the rules match, then go on...
        parent::_parse_routes();
    }
}

这就是所有的代码,我相信足够简洁、易用了。 测试程序我也提供了,在 github 上的restful-codeigniter 项目里有完整代码,使用 Git clone 一份下来就可以进行测试了。

如果发现什么不好的地方或者bug,或者有更好的实现方法,非常欢迎探讨指教。

[HOW TO] complie intl extension for php 5.3.2 under Mac OS X 10.6

2010年12月29日

近期我在研究 Symfony2,这是一个全新的、个人认为很有前途的 php 框架。和 Symfony 1.x 相比基本是推倒重来的一个版本,官方号称这是这是最快的 php 框架之一。具体可以自己上官方网站看看。

然而 Symfony2 要求 php 5.3.2+ 以上版本,还需要 php intl 库,而 Mac OS X 10.6 默认自带的 php 没有 intl 扩展,我只好自己安装。php 扩展嘛,最方便的安装方法莫过于用 pecl 安装了,然而:

$ sudo pecl install intl
downloading intl-1.1.2.tgz ...
...
/usr/temp/intl/collator/collator_class.c:92: error: duplicate ‘static’
/usr/temp/intl/collator/collator_class.c:96: error: duplicate ‘static’
/usr/temp/intl/collator/collator_class.c:101: error: duplicate ‘static’
/usr/temp/intl/collator/collator_class.c:107: error: duplicate ‘static’
make: *** [collator/collator_class.lo] Error 1
ERROR: `make' failed

Google 了一番,没什么结果,好多人在问这个问题,但就是没有一个的解决方法。

那就自力更生吧,经过一番“研究”,发现原来是 php 5.3 的使用了新的内存回收方式,zval 结构体也和 php 5.2 不一样了,一些宏也改变了,ZEND_BEGIN_ARG_INFO_EX 这个宏在 php 5.2 是这样的:

#define ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference, required_num_args) \
    zend_arg_info name[] = { \
        { NULL, 0, NULL, 0, 0, 0, pass_rest_by_reference, return_reference, required_num_args },

在 php 5.3 下是这样的:

#define ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference, required_num_args)   \
    static const zend_arg_info name[] = {                                                                       \
        { NULL, 0, NULL, 0, 0, 0, pass_rest_by_reference, return_reference, required_num_args },

而 intl 1.1.2 中很多地方都用到了这个宏,比如上面出错的 collator/collator_class.c 文件:

static
ZEND_BEGIN_ARG_INFO_EX( collator_0_args, 0, 0, 0 )
ZEND_END_ARG_INFO()

看见没有,php 5.3 中 ZEND_BEGIN_ARG_INFO_EX 的定义中已经那个包含了一个 static 关键字,而 intl 1.1.2 中用到这个宏的地方还是按照 php 5.2 的方式使用,这个宏经过预处理后,导致出现 static 关键字重复,编译出错。 知道了问题原因,就试着修改下吧。由于 intl 1.1.2 库中是用这个宏的地方很多,不好一个一个该,我就用了一个邪恶的方法,在 intl_common.h 中重新定义这个宏:

// HACK!!!
#define ZEND_BEGIN_ARG_INFO_EX(name, pass_rest_by_reference, return_reference, required_num_args)   \
    const zend_arg_info name[] = {                                                                      \
        { NULL, 0, NULL, 0, 0, 0, pass_rest_by_reference, return_reference, required_num_args },

保存再编译,这次上面的那个错误没有了,但出现了别的错误:

/Users/liang/apps/intl-1.1.2/resourcebundle/resourcebundle_class.c:215: error:‘zval’ has no member named ‘refcount’;
make: *** [resourcebundle/resourcebundle_class.lo] Error 1

这个就是上面说的 zval 结构体的改变了。php 5.3 使用了新的 GC 方式,zval 结构体中的 refcount 成员改名为 refcount__gc,is_ref 改名为 is_ref__gc,而且也有配套的管理这个新成员的宏。经过研究,intl 1.1.2 中只有 3 个地方使用 php 5.2 的方式直接访问了 refcount 成员:

$ grep -r refcount *
msgformat/msgformat_format.c:#define Z_ADDREF_P(z) ((z)->refcount++)
msgformat/msgformat_format.c:       /* TODO: needs refcount increase here? */
resourcebundle/resourcebundle_class.c:  retval->refcount--;
resourcebundle/resourcebundle_iterator.c:   object->refcount--;
resourcebundle/resourcebundle_iterator.c:   object->refcount++;

其中 msgformat/msgformat_format.c 文件已经和 php 5.3 兼容了,所以只需要修把后面的三个地方修改为 php 5.3 的方式,那这个错误应该就修改好了。 修改方法很简单,分别打开上面的几个 resourcebundle/resourcebundle_class.c 文件和 resourcebundle/resourcebundle_iterator.c 文件,按照下面的方式修改:

retval->refcount--;     => Z_DELREF_P(retval);
bject->refcount--;      => Z_DELREF_P(object);
object->refcount++;     => Z_ADDREF_P(object);

完成后重新编译,OH YEAH~~~~能通过编译了。不过别高兴得太早,可能还有问题呢。先修改一下 php.ini 中的 intl 配置为:

[intl]
intl.default_locale = zh-CN

然后运行一下测试自带的测试用例,发现有很多测试用例没通过。。。 :-(

$ make test
=====================================================================
TEST RESULT SUMMARY
---------------------------------------------------------------------
Exts skipped    :    0
Exts tested     :   55
---------------------------------------------------------------------
Number of tests :   78                78
Tests skipped   :    0 (  0.0%) --------
Tests warned    :    0 (  0.0%) (  0.0%)
Tests failed    :   17 ( 21.8%) ( 21.8%)
Expected fail   :    0 (  0.0%) (  0.0%)
Tests passed    :   61 ( 78.2%) ( 78.2%)
---------------------------------------------------------------------
Time taken      :   10 seconds
=====================================================================

=====================================================================
FAILED TEST SUMMARY
---------------------------------------------------------------------
get_locale() [tests/collator_get_locale.phpt]
collator_get_sort_key() [tests/collator_get_sort_key.phpt]
datefmt_get_pattern_code and datefmt_set_pattern_code() [tests/dateformat_get_set_pattern.phpt]
datefmt_localtime_code() [tests/dateformat_localtime.phpt]
datefmt_parse_code() [tests/dateformat_parse.phpt]
datefmt_parse_localtime() with parse pos [tests/dateformat_parse_localtime_parsepos.phpt]
datefmt_parse_timestamp_code()  with parse pos [tests/dateformat_parse_timestamp_parsepos.phpt]
datefmt_set_timezone_id_code() [tests/dateformat_set_timezone_id.phpt]
numfmt_format() [tests/formatter_format.phpt]
numfmt_format_currency() [tests/formatter_format_currency.phpt]
grapheme() [tests/grapheme.phpt]
locale_get_display_name() [tests/locale_get_display_name.phpt]
locale_get_display_region() [tests/locale_get_display_region.phpt]
locale_get_display_script() [tests/locale_get_display_script.phpt]
locale_get_display_variant() [tests/locale_get_display_variant.phpt]
locale_get_region() [tests/locale_get_region.phpt]
locale_parse_locale() [tests/locale_parse_locale.phpt]
=====================================================================

因为这些测试用例本身也是 php 程序,我就自己在命令行下运行了一遍检查为什么会导致测试失败。发现大部分输出都是正确的,但有一些情况下输出和测试用例中的断言不匹配。为什么会这样呢?我没有深入研究,也不知道为什么。哪位朋友如果熟悉这方面请赐教。

我最后的结论: 我使用了这个自己编译的版本用了一段时间,没发现有什么不正常的地方,所以这种 patch 方式应该是可行的。但是,由于存在部分测试用例没有通过,可能会隐藏着一些意想不到的bug,所以不建议在生产环境下使用。

懒人有福了,patch 后的版本我放到了 github 上,大家可以直接 clone 一份下来编译,不用自己手动修改代码了,链接在此: https://github.com/liangzhenjing/php53-intl

SELinux 导致 PHP 无法使用 fsockopen 连接到 Memcached 服务器

2010年8月5日

前段时间刚刚写了一篇关于 SELinux 导致 httpd(Apache2) 无法启动 的文章,今天又碰到 SELinux 的问题了。

事情是这样的:

首先是服务器硬盘出问题了:-(,我给换了块硬盘,然后重装系统(CentOS 5.4 i386),然后安装各种程序、还原各种数据。最后一步是使用 memcache.php 来监控 Memcache 状态。然而却发现该工具无法连接上 Memcached 服务器。经检查,Memcached 服务器已经正常启动,使用 telnet 能够正常连接上去,使用 Memcached 的应用程序(PHP程序)也正常工作。查看 memcache.php 代码发现其是使用 fsockopen 来连接 Memcached 服务器,遂怀疑 Socket 扩展的问题。然而,检查发现可以在命令行中使用 fsockopen 连接到任意地址的任意端口,说明 Socket 扩展没问题。但在 httpd 中使用 fsockopen 来就只能连接本机的 80、8080、443 端口,连接其他端口均失败。

检查 httpd 的 log 也没发现任何问题。上网搜索也没发现类似问题,郁闷ing……

于是又想到是否是 SELinux 的问题。grep 了下 /var/log/audit/audit.log,发现以下线索:

» 阅读更多: SELinux 导致 PHP 无法使用 fsockopen 连接到 Memcached 服务器

去掉 Sqlite3 数据库中的前后回车换行符(newline)

2010年7月18日

最近使用 rails 做了一个小小的小网站, 数据库用的是 sqlite3, 数据库数据是从一个 xml 文件中导入的, 有很多前导回车和末尾空行, 而页面显示的时候会自动把回车替换成 , 结果就是显示出来的内容无端端的多了很多空白, 整个页面看起来很不河蟹协调. 今天闲下来了决定把这些无用的回车换行符去掉.

不就是一个 trim 嘛, 很快进入 sqlite 命令行界面, 输入:

sqlite> update jokes set content=trim(content,’\n’);

搞定, 收工…????? 刷新页面一看, 我了个去, 不行耶~~, sqlite 不认 \n, 换成 trim(content, ‘\r’), trim(content, ‘\r\n’) 和 trim(content, ‘\n\r’) 都不行, 哥是个懒人, 不想写代码解决, 于是 google 了下, 发现有人提供这个解决方法:

x’hh’ should work, where hh are hex digits.  So if your file contained hex 0D type line breaks: UPDATE t SET essay_without_newlines = Replace(essay, x’0D’, ‘~’);

也就是说 sqlite 中可以使用 x’hh’ 这种语法来表示一个字符. 试了下 x’0D’, 不行…..再试试 x’0A’, 就好了:

sqlite> update jokes set content=trim(content, x’0A’);

流水账一笔, 没啥技术含量. 纯粹是做个备忘, 也希望对别人有所帮助.

声明: http://LiANG.eu 原创文章, 版权所有. 在保留原文来源和链接的情况下可以自由非商业转载.

推荐一个 PHP 写的 Memcache 管理器

2010年7月13日

其实,查看 memcache 服务器状态的最简单的办法是直接 telnet 上去:

[liang@iMac: ~]$ telnet localhost 11211
Trying ::1…
Connected to localhost.
Escape character is ‘^]’.
stats
STAT pid 6404
STAT uptime 8865
STAT time 1279025383
STAT version 1.2.8
……
END

但这个结果实在是太简陋了,很不直观。而且有些情况下是无法使用 telnet 连接 memcache 服务器的(比如在外网),这时候如果有一个像 PhpMyAdmin 那样的 Web 程序来管理 memcache 服务器的话,是一件非常方便的事情。

我在网上找了找,找到了这个 memcache 管理程序直接下载连接),该程序主要的功能有:

  1. 管理多个 memcache 服务器
  2. 实时查看各个服务器的内存使用状况
  3. 实时查看缓存命中情况
  4. 直接浏览缓存内容,删除指定缓存项
  5. 等等等。。。自己发掘吧

» 阅读更多: 推荐一个 PHP 写的 Memcache 管理器

PHP计算时间间隔的一个小函数

2010年7月13日

 

如果经常上开心网之类的 SNS 的话,你可能会注意到上面的很多信息并不是直接显示信息的发布时间,而是显示成为“6分钟之前”、“4天之前”这样。这种显示方法比直接显示信息的发布时间要直观得多,对用户也更有意义。

这种显示方法需要对信息的发布时间做一些处理,这里有一个PHP函数就是用来做这件事情的。此函数接受一个DateTime或者String参数,返回该参数和当前时间的间隔。注意返回的时间间隔并不是100%的和真正时间间隔对应,而是去掉了一些不太重要的部分的结果。比如如果时间间隔是 1 年 3 个月,那么只返回 “1年3个月之前”。因为一般用户是不会关心到底是 1 年3个月零几天几小时几分钟的,因为 1 年三个月已经很长了。

function datetime_diff($datetime)
{
    $datetime = is_string($datetime) ? new DateTime($datetime) : $datetime;
    $diff = date_create(‘now’)->diff($datetime);
    $suffix = ( $diff->invert ? ‘之前’ : ‘后’ );
    $diff_str = ”;

    $years      = $diff->y ? $diff->y . ‘年’ : null;
    $months     = $diff->m ? $diff->m . ‘个月’ : null;
    $days       = $diff->d ? $diff->d . ‘天’ : null;
    $hours      = $diff->h ? $diff->h . ‘小时’ : null;
    $minutes    = $diff->i ? $diff->i . ‘分钟’ : null;
    $seconds    = $diff->s ? $diff->s . ‘秒’ : null;

    if ($years)
        $diff_str = $years . $months;
    elseif ($months)
        $diff_str = $months . $days;
    elseif ($days)
        $diff_str = $days . $hours;
    elseif ($hours)
        $diff_str = $hours . $minutes;
    else
        $diff_str = $minutes . $seconds;

    return $diff_str . $suffix;
}

使用方法:

[liang@iMac: ~]$ php -a
Interactive shell

php > echo datetime_diff(’2012-06-04 08:09:00′);                  // 1年10个月后
php > echo datetime_diff(’2000-07-08 09:10:11′);                  // 10年之前
php > echo datetime_diff(’2010-01-12 16:04:16′);                  // 6个月1天之前
php > echo datetime_diff(’2010-07-08 09:10:11′);                  // 5天7小时之前
php > echo datetime_diff(’2010-07-13 16:32:16′);                  // 59秒之前
php > echo datetime_diff(new DateTime(’2010-07-08 09:10:11′));    // 5天7小时之前
php > echo datetime_diff(new DateTime(’2010-07-13 14:15:16′));    // 2小时17分钟之前
php > echo datetime_diff(new DateTime(’2010-01-13 16:04:16′));    // 6月之前

注意:

此方法使用到了 DateInterval 类的几个 Undocumented 的属性(y, m, d, h, i, s),在以后的版本中有可能会导致兼容性问题,这里只是提供一个思路,请谨慎使用。

 

例行公事:http://LiANG.eu 原创文章,转载请保留原文链接,否则。哼哼。。。