解决 PHP 中 usort 在值相同时改变原始位置的问题

从 PHP 4.1.0 后,usort 在比较的值相同时,原始位置可能会改变,文档中是这样说的:

If two members compare as equal, their order in the sorted array is undefined.

也就是说,如果比较的2个值相同,则它们在排序结果中的顺序是随机的。如果你需要保持相同值的原始位置,可以参考本文的方法。

演示数据:

<?php
/*
解决 PHP 中 usort 在值相同时改变原始位置的问题
作者:Artlover    http://www.CodeBit.cn
*/
$arr = array(
	array('a' => 5, 'b' => 3),
	array('a' => 5, 'b' => 1),
	array('a' => 5, 'b' => 4),
	array('a' => 5, 'b' => 2),
);
?>

数组中第一个元素的值是相同的,期望的结果是保持现有的位置不变,即 b 的顺序为 3,1,4,2

用 usort 排序,当比较字段的值相同时,原始顺序可能会改变

<?php
/*
解决 PHP 中 usort 在值相同时改变原始位置的问题
作者:Artlover    http://www.CodeBit.cn
*/
$callback = create_function('$a,$b', 'return ($a["a"] == $b["a"])?0:(($a["a"] > $b["a"]) ? 1 : -1);');
usort($arr, $callback);
?>

结果:


Array
(
    [0] => Array
        (
            [a] => 5
            [b] => 2
        )
 
    [1] => Array
        (
            [a] => 5
            [b] => 4
        )
 
    [2] => Array
        (
            [a] => 5
            [b] => 1
        )
 
    [3] => Array
        (
            [a] => 5
            [b] => 3
        )
 
)

虽然排序字段的值相同,但是 usort 却将整个数组的顺序打乱了。

如果要在比较的值相同时保持原始位置,可以用 array_multisort :

<?php

/*
解决 PHP 中 usort 在值相同时改变原始位置的问题
作者:Artlover    http://www.CodeBit.cn
*/

// 索引计数器
$i = 0;

// 创建2个空数组,第一个保存要排序的字段,第二个保存原始索引信息
$a = $index = array();

foreach ($arr as $key => $data) {
	$a[$key] = $data['a'];
	$index[] = $i++;
}

// 第一个数组先排,接着按原始索引排
array_multisort($a, SORT_ASC, $index, SORT_ASC, $arr);

?>

结果:


Array
(
    [0] => Array
        (
            [a] => 5
            [b] => 3
        )
 
    [1] => Array
        (
            [a] => 5
            [b] => 1
        )
 
    [2] => Array
        (
            [a] => 5
            [b] => 4
        )
 
    [3] => Array
        (
            [a] => 5
            [b] => 2
        )
 
)

PHP 中检查或过滤 IP 地址

网络环境异常复杂,有时候我们不得不禁止一些恶意用户访问,禁止的方式有很多种,其中一种就是通过 IP 来限制,本文提供的方法允许你通过 IP 区间、CIDR (Classless Inter-Domain Routing)及单个 IP 格式来检查或过滤 IP 地址。

你可以通过增加一个配置文件,然后将需要禁止的一些 IP 地址通过一定规则添加到配置文件中,在程序初始化的时候,读取配置文件中的每个规则,然后通过本文提供的方法去检查当前访问的客户端 IP 地址是否存在于这些规则中,如果存在,则拒绝提供服务。

<?php
/**
 * PHP 中检查或过滤 IP 地址
 * 
 * 支持 IP 区间、CIDR(Classless Inter-Domain Routing)及单个 IP 格式
 * 整理:http://www.CodeBit.cn
 * 参考:
 *   - {@link http://us2.php.net/manual/zh/function.ip2long.php#70055}
 *   - {@link http://us2.php.net/manual/zh/function.ip2long.php#82397}
 * 
 * @param string $network 网段,支持 IP 区间、CIDR及单个 IP 格式
 * @param string $ip 要检查的 IP 地址
 * @return boolean
 */
function netMatch($network, $ip) {

    $network = trim($network);
    $ip = trim($ip);
    
    $result = false;
    
    // IP range : 174.129.0.0 - 174.129.255.255
    if (false !== ($pos = strpos($network, "-"))) {
        $from = ip2long(trim(substr($network, 0, $pos)));
        $to = ip2long(trim(substr($network, $pos+1)));
        
        $ip = ip2long($ip);
        
        $result = ($ip >= $from and $ip <= $to);

    // CIDR : 174.129.0.0/16
    } else if (false !== strpos($network,"/")) {
        list ($net, $mask) = explode ('/', $network);
        $result = (ip2long($ip) & ~((1 << (32 - $mask)) - 1)) == ip2long($net); 

    // single IP
    } else {
        $result = $network === $ip;
    
    }
    
    return $result;
}

// 174.129.0.0 - 174.129.255.255
var_dump(netMatch(' 174.129.0.0  -  174.129.255.255 ', '174.129.1.31')); // True
var_dump(netMatch(' 174.129.0.0/16 ', '174.139.1.31')); // False
var_dump(netMatch(' 174.129.1.32 ', '174.129.1.31')); // False
?>

由于中国使用的大多数都是动态 IP 地址,所以通过 IP 地址限制访问具有一定的局限性,使用的时候需要谨慎,但是对于应急限制访问来说,还是非常有用的。

PHP 的 array_diff() 函数在处理大数组时的效率问题

PHP 5.2.6 以上版本的 array_diff() 函数在处理大数组时,需要花费超长时间,这个 bug 已经被官方确认;在这个问题被修复之前或者在我们不能控制 PHP 版本的时候,可以使用本文提供的方法。

cisa 提交到 PHP 官方 BUG 页面上的方法

<?php
/**
 * 解决 php 5.2.6 以上版本 array_diff() 函数在处理
 * 大数组时的需要花费超长时间的问题
 * 
 * 整理:http://www.CodeBit.cn
 * 来源:http://bugs.php.net/47643
 */
function array_diff_fast($data1, $data2) {
    $data1 = array_flip($data1);
    $data2 = array_flip($data2);

    foreach($data2 as $hash => $key) {
       if (isset($data1[$hash])) unset($data1[$hash]);
    }

    return array_flip($data1);
}
?>

根据 ChinaUnix 论坛版主 hightman 思路重写的方法

<?php
/**
 * 解决 php 5.2.6 以上版本 array_diff() 函数在处理大数组时的效率问题
 * 根据 ChinaUnix 论坛版主 hightman 思路写的方法
 * 
 * 整理:http://www.CodeBit.cn
 * 参考:http://bbs.chinaunix.net/viewthread.php?tid=938096&rpid=6817036&ordertype=0&page=1#pid6817036
 */
function array_diff_fast($firstArray, $secondArray) {

    // 转换第二个数组的键值关系
    $secondArray = array_flip($secondArray);

    // 循环第一个数组
    foreach($firstArray as $key => $value) { 

        // 如果第二个数组中存在第一个数组的值
        if (isset($secondArray[$value])) {

            // 移除第一个数组中对应的元素
            unset($firstArray[$key]); 
        } 
    }

    return $firstArray; 
}
?>

此方法只交换了第二个数组的 key 和 value,所以效率更高。

注意:PHP 内置的 array_diff() 函数可以处理多个数组,而本文提供的方法只处理了两个数组的比较。

在 XSLTProcessor 中 registerPHPFunctions 后无法调用 php 函数

XSLT 是一个非常方便的转换 XML 的工具,PHP 里面是通过 XSLTProcessor 来实现;XSLT 中内置了许多有用的函数,同时,只需要调用 XSLTProcessor 实例的 registerPHPFunctions 方法,我们就可以在 XSLT 中直接使用 PHP 的函数,这大大增强了 XSLT 的处理能力。

但是,在 XSLT 中使用 PHP 函数时,很多人会遇到如下两种错误:

(1) Warning: XSLTProcessor::transformToXml(): xmlXPathCompiledEval: 1 objects left o
n the stack.
(2)PHP Warning: XSLTProcessor::transformToXml(): xmlXPathCompOpEval: function func
tion bound to undefined prefix php in ….

示例代码:

 
<?php
$xml = <<<EOB
<allusers>
 <user>
  <uid>bob</uid>
 </user>
 <user>
  <uid>joe</uid>
 </user>
</allusers>
EOB;
$xsl = <<<EOB
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" 
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:output method="html" encoding="utf-8" indent="yes"/>
 <xsl:template match="allusers">
  <html><body>
    <h2>Users</h2>
    <table>
    <xsl:for-each select="user">
      <tr><td>
        <xsl:value-of
             select="php:function('ucfirst',string(uid))"/>
      </td></tr>
    </xsl:for-each>
    </table>
  </body></html>
 </xsl:template>
</xsl:stylesheet>
EOB;
$xmldoc = DOMDocument::loadXML($xml);
$xsldoc = DOMDocument::loadXML($xsl);
 
$proc = new XSLTProcessor();
$proc->registerPHPFunctions();
$proc->importStyleSheet($xsldoc);
echo $proc->transformToXML($xmldoc);
?>

其实,出现这种错误,是因为我们没有定义 PHP namespace ,只需要在

 
<xsl:stylesheet version="1.0" 
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform">

中增加 xmlns:php="http://php.net/xsl" 就能解决此问题, 即

 
<xsl:stylesheet version="1.0" 
     xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
     xmlns:php="http://php.net/xsl">

在 Zend Framework MVC 下禁用 view 或者 layout

在 Zend_Controller 中禁用 view

在 Action 级别禁用 view:

 
<?php
class FooController extends Zend_Controller_Action
{
    public function barAction()
    {
         $this->_helper->viewRenderer->setNoRender();
    }
}
?>

在执行当前 action 的时候会不会展示 view .

在 Controller 级别禁用 view:

 
<?php
class FooController extends Zend_Controller_Action
{
    public function init()
    {
         $this->_helper->viewRenderer->setNoRender();
    }
}
?>

在执行当前 controller 下的所有 action 的时候都不会展示 view .

全局级别禁用 view:

 
<?php
Zend_Controller_Front::getInstance()->setParam('noViewRenderer', true);
?>

在整个程序的执行过程中都不会展示 view .

在 Controller 中禁用或改变 layout

禁用 layout

 
<?php
class FooController extends Zend_Controller_Action
{
    public function barAction()
    {
        $this->_helper->layout->disableLayout();
    }
}
?>

在此 action 执行的时候将不会使用 Zend_Layout 。

改变 layout

 
<?php
class FooController extends Zend_Controller_Action
{
    public function barAction()
    {
        $this->_helper->layout->setLayout('other');
    }
}
?>

在此 action 执行的时候将使用名为 other 的 layout 。

Zend Framework 中配合 Zend_Config 实现路由(Router)规则单独存储

Zend Framework 的路由转发功能不仅可以让 url 变得简洁易记,而且非常对 SEO 非常有益,zf 的路由转发有着多样的配置方法,能实现各种转发需求。

随着站点复杂程度的增加,我们会有越来越多的转发规则,而将这些规则独立到一个文件中,是一个非常好的习惯。本文讲解的是将转发规则存放在单独的配置文件里面,然后配合 Zend_Config 实现转发的方法。

对于直接在程序中实现转发设置,手册上有简单的示例:

 
<?php
 
/*
说明:设置 Zend Framework 路由转发
整理:CodeBit.cn [ http://www.codebit.cn ]
*/
 
$router = $controller->getRouter(); // 获取路由
$route = new Zend_Controller_Router_Route(
    'author/:username',
    array(
        'controller' => 'profile',
        'action'     => 'userinfo'
    )
);
 
$router->addRoute('user', $route);
 
?>

上面的规则能将 http://domain.com/author/martel 指向到 http://domain.com/profile/action/author/martel ,这样设置,能大大降低 url 的复杂程度,让 url 简单明了。

我们如果有多条这种规则,就可以考虑将规则放到单独的文件中,手册上是用 ini 文件存储路由规则,我这里则使用 php 数组存储:

 
<?php
 
/*
说明:用单独的 php 文件存储路由转发规则
整理:CodeBit.cn [ http://www.codebit.cn ]
*/
 
return array(
 
	// 将 /view/123 映射为 /default/index/index/id/123
	'view'	=>	array(
		'route'	=>	'view/:id',
		'defaults'	=>	array(
			'module'	=>	'default',
			'controller'	=>	'index',
			'action'	=>	'index',
		),
	),
 
	// 将 http://domain.com/author/martel 映射为 http://domain.com/profile/action/author/martel
	'profile'	=>	array(
		'route'	=>	'author/:username',
		'defaults'	=>	array(
			'controller'	=>	'profile',
			'action'	=>	'userinfo',
		),
	),
	
);
 
?>

对于从数组中取出规则,然后设置 Zend_Controller_Router_Rewrite ,手册也有一段示例:

 
<?php
 
$routerConfig = new Zend_Config(require_once 'routes.php');
$routerRules = new Zend_Controller_Router_Rewrite();
$router->addConfig($routerRules);
 
?>

不过,手册上没有如何将上面的路由规则应用到当前 controller 的示例,这个也是让新手很头疼的问题,研究过 zend framework 的源码后,我们可以很容易发现解决方法:

 
<?php
 
/*
说明:用单独的 php 文件存储路由转发规则
整理:CodeBit.cn [ http://www.codebit.cn ]
*/
 
$routerConfig = new Zend_Config('routes.php'); // 加载配置
$routerRules = new Zend_Controller_Router_Rewrite();
$routerRules->addConfig($routerConfig); // 设置规则
 
$routers = $controller->getRouter(); // 获取路由
$routers->addRoutes($routerRules->getRoutes()); // 通过 $routerRules->getRoutes() 获取规则,然后设置
 
?>

MySQL 中将一个表的数据插入另外一个表的方法

开发中,我们经常需要将一个表的数据插入到另外一个表,有时还需要指定导入字段,虽然这个实现起来非常简单,但是还是会困扰许多新手,因此专门发一篇文章备查。

如果2张表的字段一致,并且希望插入全部数据,可以用这种方法:

 
INSERT INTO 目标表 SELECT  * FROM 来源表;

比如要将 articles 表插入到 newArticles 表中,则是:

 
INSERT INTO newArticles SELECT  * FROM articles;

如果只希望导入指定字段,可以用这种方法:

 
INSERT INTO 目标表 (字段1, 字段2, ...) SELECT  字段1, 字段2, ...  FROM 来源表;

注意字段的顺序必须一致。

如果您需要只导入目标表中不存在的记录,可以参考另外一篇文章

MySQL 当记录不存在时插入(insert if not exists)

在 Zend Framework 应用中使用 Smarty 做模版引擎

Zend Framework 是 PHP 官方出的开发框架,随着 PHP5 的推广,越来越多的人开始学习、使用 Zend Framework。 Smarty 是一个非常强大的模版引擎,由于其完善的语法、内置的缓存功能,已经被众多开发人员应用在自己的开发项目中。

正因为二者的强大,因此有越来越多的人开始考虑将 Smarty 集成到 Zend Framework ,以便充分的发挥各自的优点。本文翻译自 Zend Developer Zone 。翻译为意译,由于水平有限,纰漏在所难免,欢迎指出翻译中出现的问题。本文为 CodeBit.cn ( http://www.codebit.cn ) 组织翻译,转载请注明出处。

原文作者首先指明了整合的目的:(1)保留 Zend_View 提供的 API;(2)利用 Smarty 提供的缓存功能。

目录结构:

文件名是按照作者的公司(Travello)命名:

 
  /Zend
    /View
  /Travello
    /View
      Smarty.php

定义类和构造函数:
首先是定义类和构造函数, Travello_View_Smarty 是继承自 Zend_View_Abstract,在构造函数中首先调用了父类的构造函数,然后将初始化好的 Smarty 对象存储在 private 属性中。

注意:这里使用了 configuration 对象存取 Smarty 的配置信息。

 
<?php
 
class Travello_View_Smarty extends Zend_View_Abstract
{
    private $_smarty = false;
    
    public function __construct($data = array())
    {
        parent::__construct($data);
        
        $config = Zend::registry('config');
        
        $this->_smarty = new Smarty();
        
        $this->_smarty->caching = $config->getSetting('smarty', 'caching');
        $this->_smarty->cache_lifetime = $config->getSetting('smarty', 'cache_lifetime');
        $this->_smarty->template_dir = $config->getSetting('smarty', 'template_dir');
        $this->_smarty->compile_dir = $config->getSetting('smarty', 'compile_dir');
        $this->_smarty->config_dir = $config->getSetting('smarty', 'config_dir');
        $this->_smarty->cache_dir = $config->getSetting('smarty', 'cache_dir');
    }

实现 _run() 方法:
_run() 是唯一的需要在 Zend_View_Abstract 子类中实现的方法,他会自动在 render() 方法中被调用,这里使用了 Smarty 的 display() 方法来输出模版。

 
<?php
 
    protected function _run($template)
    {
        $this->_smarty->display($template);
    }

重写 assign() 方法:
下面是要重写 Zend_View_Abstract 下面的 assign() 方法,我们直接将参数值赋到 Smarty 对象,而不是默认的 Zend_View_Abstract 的 $this->_vars 数组。

 
<?php
 
    public function assign($var)
    {
        if (is_string($var))
        {
            $value = @func_get_arg(1);
            
            $this->_smarty->assign($var, $value);
        }
        elseif (is_array($var))
        {
            foreach ($var as $key => $value)
            {
                $this->_smarty->assign($key, $value);
            }
        }
        else
        {
            throw new Zend_View_Exception('assign() expects a string or array, got '.gettype($var));
        }
    }

重写 escape() 方法:
下面是要重写 Zend_View_Abstract 下面的 escape() 方法,使用的是 Zend_View_Abstract 内置的 escape() 方法,但是能够根据传入的字符串或者数组作出相应处理。

 
<?php
 
    public function escape($var)
    {
        if (is_string($var))
        {
            return parent::escape($var);
        }
        elseif (is_array($var))
        {
            foreach ($var as $key => $val)
            {
                $var[$key] = $this->escape($val);
            }
 
            return $var;
        }
        else
        {
            return $var;
        }
    }

打印输出:
output() 是对 Zend_View_Abstract 中 render() 方法的封装,增加了一些 header 设置:

 
<?php
 
    public function output($name)
    {
        header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
        header("Cache-Control: no-cache");
        header("Pragma: no-cache");
        header("Cache-Control: post-check=0, pre-check=0", FALSE);
 
        print parent::render($name);
    }

使用 Smarty 的缓存:
最后的2个方法是为了在 View 类中整合 Smarty 的缓存机制,第一个方法是检查是否已经缓存,第二个是设置是否打开缓存功能。

 
<?php
 
    public function isCached($template)
    {
        if ($this->_smarty->is_cached($template))
        {
            return true;
        }
        
        return false;
    }
 
    public function setCaching($caching)
    {
        $this->_smarty->caching = $caching;
    }
} // end Travello_View_Smarty
 
?>

上面是完整的 Travello_View_Smarty 代码。

使用说明:
使用非常简单,在你的导入文件(通常是 index.php ,译注)中,可以用下面的方法初始化, $viewConfig 是设置 Zend_View 的路径,一旦创建好对象后,就将他存在对象中,方便后面调用。

 
<?php
 
$viewConfig = array();
$viewConfig['scriptPath'] = $config->getSetting('framework', 'view_dir');
 
$view = new Travello_View_Smarty($viewConfig);
Zend::register('view', $view);
 
?>

在你的 controller 和 action 方法中可以这样使用:

 
<?php
 
    public function indexAction()
    {
        $temp_file = 'homepage.htm';
        
        $view = Zend::registry('view');
        
        if (false === $view->isCached($temp_file))
        {
            $vars = array();
            $vars['title'  ] = 'Page <title>';
            $vars['text'   ] = 'A text is a text & a text is a text.';
            $vars['numbers'][0] = 12.9;
            $vars['numbers'][1] = 29;
            $vars['numbers'][2] = 78;
            
            $vars = $view->escape($vars);
            
            $view->assign($vars);
        }
        
        $view->output($temp_file);
    }
 
?>

原文地址:
http://devzone.zend.com/node/view/id/120

批量替换 MySQL 指定字段中的字符串

批量替换 MySQL 指定字段中的字符串是数据库应用中很常见的需求,但是有很多初学者在遇到这种需求时,通常都是用脚本来实现;其实,MySQL 内置的有批量替换语法,效率也会高很多;想了解具体方法,继续阅读本文吧 :)

批量替换的具体语法是:

 
UPDATE 表名 SET
指定字段 = replace(指定字段, ’要替换的字符串’, ’想要的字符串’) 
WHERE 条件;

如果你想把 article 表中 ID 小于5000的记录,content 字段中“解决”替换成“解放”,那么语法就是:

 
UPDATE article SET
content = replace(content, ’解决’, ’解放’) 
WHERE ID<5000;

是不是很方便 :)

获取 $_SERVER[“REQUEST_URI”] 值的通用解决方案

在 PHP 众多预定义服务器变量中,$_SERVER["REQUEST_URI"] 算是经常用到的,但是这个变量只有 apache 才支持,因此,我们需要一个更加通用的方式来获取 REQUEST_URI 的值,本文就是结束这一问题的解决方案。

 
<?php
 
// 说明:获取 _SERVER['REQUEST_URI'] 值的通用解决方案
// 来源:drupal-5.1 bootstrap.inc
// 整理:CodeBit.cn ( http://www.CodeBit.cn )
 
function request_uri()
{
	if (isset($_SERVER['REQUEST_URI']))
	{
		$uri = $_SERVER['REQUEST_URI'];
	}
	else
	{
		if (isset($_SERVER['argv']))
		{
			$uri = $_SERVER['PHP_SELF'] .'?'. $_SERVER['argv'][0];
		}
		else
		{
			$uri = $_SERVER['PHP_SELF'] .'?'. $_SERVER['QUERY_STRING'];
		}
	}
	return $uri;
}
 
?>