2009年6月1日星期一

Zend_Paginator 空适配器 bug

Zend_Paginator_Adapter_Null 可以让你不使用 Zend_Paginator 来管理数据而仍然能够使用 分页控制。但zf 1.6-1.8.2 存在最后一页数据不对的情况。

Test code:

$paginator = Zend_Paginator::factory(2);
$paginator->setCurrentPageNumber(1);
$paginator->setItemCountPerPage(5);

$pages = $paginator->getPages();
var_dump($pages);


期望结果:
["currentItemCount"]=>2
["lastItemNumber"]=>2
实际结果:
["currentItemCount"]=>5
["lastItemNumber"]=>5

修正方法:
public function getItems($offset, $itemCountPerPage)
{
if ($this->_count <= $offset) return array();
$remainItemCount = $this->_count - $offset;
$currentItemCount = $remainItemCount > $itemCountPerPage ? $itemCountPerPage : $remainItemCount;
return array_fill(0, $currentItemCount, null);
}

2009年4月23日星期四

Zend Framework Plugin: 不同模块(Module)使用不同的错误控制器(ErrorController)

Zend Framework 的错误控制器提供了统一的错误处理方式, 参阅 http://framework.zend.com/manual/en/zend.controller.html#zend.controller.quickstart.go.errorhandler
  当我们工作于模块方式时,常常希望不同模块使用不同的错误处理,比如 default 模块使用默认ErrorController, 后台管理模块 admin 使用 Admin_ErrorController 提供不同的布局和更详细的错误显示。

Plugin 方式为我们提供了方便的设置途径,以下 plugin 设置每个 module 使用 模块里的 ErrorController, 如果模块不提供 ErrorController 则使用默认的 ErrorController:
require_once ('Zend/Controller/Plugin/Abstract.php');
class Mezi_Controller_Plugin_ErrorControllerSelector extends Zend_Controller_Plugin_Abstract
{
    public function routeShutdown(Zend_Controller_Request_Abstract $request)
    {
        $front = Zend_Controller_Front::getInstance();

        //If the ErrorHandler plugin is not registered, bail out
        if( !($front->getPlugin('Zend_Controller_Plugin_ErrorHandler') instanceof Zend_Controller_Plugin_ErrorHandler) )
            return;

        $error = $front->getPlugin('Zend_Controller_Plugin_ErrorHandler');

        //Generate a test request to use to determine if the error controller in our module exists
        $testRequest = new Zend_Controller_Request_HTTP();
        $testRequest->setModuleName($request->getModuleName())
                    ->setControllerName($error->getErrorHandlerController())
                    ->setActionName($error->getErrorHandlerAction());

        //Does the controller even exist?
        if ($front->getDispatcher()->isDispatchable($testRequest)) {
            $error->setErrorHandlerModule($request->getModuleName());
        }
    }
}


启动时安装 plugin:
$front = Zend_Controller_Front::getInstance();
$front->registerPlugin(new Mezi_Controller_Plugin_ErrorControllerSelector());




Zend Framework Plugin:自适应 magic_quotes_gpc 环境

推荐关闭 php.ini 的 magic_quotes_gpc 选项,一来可以提升性能,二来可以保证得到的原始数据,Zend_Db 会自动转义数据入库。并且新版本的 php 推荐配置都是默认关闭 magic_quotes_gpc 选项。
当然某些系统打开了 magic_quotes_gpc 选项,并且你也没有足够权限去修改这个配置,这些反斜杠可能给你造成困扰,我们可以用代码抵消修改。
Zend Framework 的 plugin 提供了很好的方式来做这些事情,而不需要改动我们去参数的每段代码。

/**
* A Zend Controller Plugin dedicated to undoing the damage of magic_quotes_gpc
* in systems where it is on.
*
* @author Ken
* @version $Id:$
*/
require_once ('Zend/Controller/Plugin/Abstract.php');
class Mezi_Controller_Plugin_StripMagicQuotes extends Zend_Controller_Plugin_Abstract
{
/**
* strip all slashes off $request parameters
*
* @param Zend_Controller_Request_Abstract $request
*/
public function dispatchLoopStartup(Zend_Controller_Request_Abstract $request)
{
$params = $request->getParams();
array_walk_recursive($params, array($this , '_stripSlashes'));
$request->setParams($params);

if ($request instanceof Zend_Controller_Request_Http) {
$this->_stripSlashesGPC();
}
}

/**
* strip all slashes from GPC
*/
protected function _stripSlashesGPC()
{
array_walk_recursive($_GET, array($this , '_stripSlashes'));
array_walk_recursive($_POST, array($this , '_stripSlashes'));
array_walk_recursive($_COOKIE, array($this , '_stripSlashes'));
}

/**
* callback strip the slashes off an item in the Params array
*
* @param string $value
* @param string $key
*/
protected function _stripSlashes (&$value, $key)
{
$value = stripslashes($value);
}
}


然后启动时安装 plugin, 仅在 magic_quotes_gpc 打开时需要处理:

$front = Zend_Controller_Front::getInstance();

if (get_magic_quotes_gpc()) {
require_once 'Mezi/Controller/Plugin/StripMagicQuotes.php';
$this->_front->registerPlugin(new Mezi_Controller_Plugin_StripMagicQuotes());
}


Plugin 非常漂亮的实现了我们的需求,以前写好的代码都无须改动。

2009年3月23日星期一

PHP 中的 UTF8 和 Mysql

很多人在 Mysql 默认为非 UTF8 但要使用 UTF8 编码的时候会使用 "SET NAMES UTF8" 语句去设置正确的编码方式。通常为了方便在初始化 Db 对象或者创建连接后就去执行,虽然这种方式可以正确工作,但会导致一次连接和查询,即使初始化之后并无需要数据库。这也破坏了很多数据库对象提供的 lazy connect 特性。
PHP 手册不推荐使用 query() 去执行 SET NAMES

更好的方法是使用数据库驱动自身提供的特性去初始化编码。

mysql:
mysql_set_charset('utf8');

mysqli:
mysqli_set_charset($link, 'utf8'); //面向过程风格
$mysql->set_charset('utf8'); //或者面向对象风格
//也可以使用初始化命令
mysqli_options($link, MYSQLI_INIT_COMMAND, 'SET NAMES utf8'); //面向过程
$mysql->options(MYSQLI_INIT_COMMAND, 'SET NAMES utf8'); //面向对象


PDO:
//使用连接参数,MYSQL_ATTR_INIT_COMMAND 在连接后执行初始化命令,类似于 query 好处是仅在连接后执行一次
$pdo = new PDO("mysql:host=localhost;dbname=dbname",
'username', 'password',
array(PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES utf8"));


Zend_Db,Zend_Db 没有提供特殊的设置参数,只能使用初始化命令参数, 见 Mysqli 和 PDO 的 INIT_COMMAND:
//PDO 驱动
$params = array(
'host' => 'localhost',
'username' => 'username',
'password' => 'password',
'dbname' => 'dbname',
'driver_options' => array(PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES UTF8');
);
$db = Zend_Db::factory('Pdo_Mysql', $params);

//Mysqli 驱动:
$params = array(
'host' => 'localhost',
'username' => 'username',
'password' => 'password',
'dbname' => 'dbname',
'driver_options' => array(MYSQLI_INIT_COMMAND => 'SET NAMES UTF8');
);
$db = Zend_Db::factory('Mysqli', $params);


2009年2月25日星期三

Smarty and Zend_View integrantion

聚合使用 smarty

<?php
require_once ('Zend/View/Abstract.php');
class SmartyView extends Zend_View_Abstract
{
protected $_smarty;

public function __construct($config = array())
{
$this->_smarty = new Smarty();

if(!isset($config['compileDir']))
throw new Exception('compileDir is not set for '.get_class($this));
else
$this->_smarty->compile_dir = $config['compileDir'];

if(isset($config['configDir']))
$this->_smarty->config_dir = $config['configDir'];

if(isset($config['pluginsDir']))
$this->_smarty->plugins_dir[] = $config['pluginsDir'];

parent::__construct($config);
}

public function __set($key,$val)
{
parent::__set($key, $val);
$this->_smarty->assign($key,$val);
}

public function __isset($key)
{
$var = $this->_smarty->get_template_vars($key);
if($var)
return true;

return false;
}

public function __unset($key)
{
parent::__unset($key);
$this->_smarty->clear_assign($key);
}

public function assign($spec,$value = null)
{
if($value === null)
$this->_smarty->assign($spec);
else
$this->_smarty->assign($spec,$value);
}


public function clearVars()
{
$this->_smarty->clear_all_assign();
}

protected function _run()
{
$this->strictVars(true);

//why 'this'?
//to emulate standard zend view functionality
//doesn't mess up smarty in any way
$this->_smarty->assign_by_ref('this',$this);

$fileFullname = func_get_arg(0);
$templateDirs = $this->getScriptPaths();

//TODO: find more effective way to get correct template dir
$templateDir = $templateDirs[0];
$file = $fileFullname;
foreach ($templateDirs as $dir) {
if (preg_match("|^$dir|", $fileFullname)) {
$templateDir = $dir;
$file = substr($fileFullname,strlen($templateDir));
break;
}
}
//$file = substr(func_get_arg(0),strlen($templateDir));
//var_dump($templateDir, $file);
$this->_smarty->template_dir = $templateDir;
$this->_smarty->compile_id = $templateDir;

echo $this->_smarty->fetch($file);
}
}


初始化脚本中设置 view

//Create the view and set the compile dir to template_c
$view = new SmartyView(array(
'compileDir' => './template_c'
));

//Create a new ViewRenderer helper and assign our newly
//created SmartyView object as the view instance
$viewHelper = new Zend_Controller_Action_Helper_ViewRenderer($view);
$viewHelper->setViewSuffix('tpl');

//Save the helper to the HelperBroker
Zend_Controller_Action_HelperBroker::addHelper($viewHelper);

2009年1月19日星期一

SPL DirectoryIterator

DirectoryIterator 提供目录迭代功能。可替换 opendir() 和 dir 伪类功能。

opendir() 写法:
$path = '.';
if (is_dir($path)) {
if ($dh = opendir($path)) {
while (false !== ($file = readdir($dh))) {
echo "filename: $file \tfiletype: ", filetype($path . $file), "\n";
}
closedir($dh);
}
}


dir 伪类写法:
$path = '.';
$dir = dir($path);
while (false !== ($entry = $dir->read())) {
echo $entry, "\n";
}
$dir->close();


主角出场:
$path = '.';
$dir = new DirectoryIterator($path);
foreach ($dir as $fileinfo) {
echo 'filename:', $fileinfo->getFilename(), "\tfiletype:", $fileinfo->getType(), "\n";
}


参考资料:
http://www.php.net/manual/en/class.directoryiterator.php
http://www.php.net/manual/en/class.splfileinfo.php
http://www.php.net/~helly/php/ext/spl/

2009年1月8日星期四

SPL File Object

class SplFileObject extends SplFileInfo implements RecursiveIterator, SeekableIterator
SPL 文件对象继承自 SplFileInfo, 实现了 RecursiveIterator, SeekableIterator 接口。

一般我们访问文件方式:

$filename = '/path/ to/file';
$fp = fopen($filename, 'r');
if ($fp) {
while (!feof($fp)) {
$text = fgets($fp);
echo $text;
}
fclose($fp);
}


利用 SplFileObject 我们可以这么写:

$filename = '/path/ to/file';
$file = new SplFileObject($filename);
$file->openFile();
foreach ($file as $line) {
echo $line;
}

因为 SplFileObject 实现了RecursiveIterator 迭代器接口, foreach 可以很方便的访问到它。

写文件:

$filename = '/path/to/file';
$file = new SplFileObject($filename);
$file->openFile('w');
$file->fwrite('some text');


继承自 SplFileInfo 的一些方法:

echo '修改时间:', $file->getMTime(), "\n";
echo '所在路径:', $file->getPath(), "\n";
echo '文件名:', $file->getFilename(), "\n";
echo '全路径(路径+文件名):', $file->getPathname(), "\n";



更多方法请参考 Spl 文档