PHP 中数组获取不到元素

早上看到 SO 上一个有关 PHP 的问题,提问者描述有一个数组,使用 print_r 可以看到索引 key 和相对应的 value 都是存在的,但是访问该元素,不管是使用 array[key] 还是 array[‘key’] 这两种访问形式,都提示 Undefined offset 而取不到数据。举例描述提问者的问题,假设一个数组 $a,print_r($a) 的输出为

Screenshot from 2013-11-28 21:19:19

可以看到数组存在索引 1 值为 foo,当使用 $a[1] 或者 $a[‘1’] 访问索引为 1 的元素,都提示 Undefined offset,这就有点让人费解了,下文将讲解这个问题产生的原因,以及如何得到像这样奇怪的一个数组。

首先说明一点,PHP 中数组的 key 可以为整形和字符串,但是包含有合法整型值的字符串会被转换为整型。例如键名 “1” 实际会被储存为 1。来看一个例子,考虑如下代码:

$a = array(
    1      => 'foo',
    '1'    => 'bar',
    'name' => 'upliu',
);
print_r($a);
var_dump($a);

将会输出:

Screenshot from 2013-11-28 21:43:06

可以看到存入到数组里面的 1 为数值索引(注意索引 name 加了引号,说明索引 name 为字符串索引(这不废话嘛,’name’ 肯定是字符串啊)),并且值为 bar 覆盖了先出现的 foo,$a[1] 和 $a[‘1’] 都能正确读取到 bar,且没有任何错误警告提示,说明这个两者都是可用的(笔者在此猜测 $a[‘1’] 实际上完全等效于 $a[1],PHP 数组读取元素的时候会将数值字符串索引转换为数值索引)。

我们先还原一下提问者的问题,看如何生产出那样一个数组。考虑如下代码:

$json = '{"1":"foo"}';
$o = json_decode($json);
var_dump($o);

将会输出

Screenshot from 2013-11-28 21:57:15

这个结果很显而易见,$o 为一个对象,有一个属性为 1,因为该属性并不是合法的 PHP 标识符,因此不能使用箭头的方式访问,我们使用强制类型转换将该对象转换为一个数组:

$a = (array)$o;
print_r($a);

将会输出

Screenshot from 2013-11-28 22:03:35

接下来尝试访问数组 $a 的索引为 1 的元素:

echo $a[1], PHP_EOL;
echo $a['1'], PHP_EOL;

上面两条语句均会报错 Undefined offset,这时数组 $a 就是 SO 上那位提问者遇到问题时碰到的数组了,BUG 重现是一件很爽的事啊。

我们来直接将上面代码中的 json 串解析为数组:

$a2 = json_decode($json, true);
print_r($a2);
echo $a2[1], PHP_EOL;
echo $a2['1'], PHP_EOL;

将会输出

Screenshot from 2013-11-28 22:11:16

一切正常,这个时候问题来了,明明数组 $a 和数组 $a2 使用 print_r 输出一模一样,为什么一个元素可以访问,另一个却不能访问。我们用更强大的 var_dump 看看:

var_dump($a);
var_dump($a2);

将会输出

Screenshot from 2013-11-28 22:14:36

从这个输出我们可以看到数组 $a 和 $a2 的不同,通过将对象强制类型转换得到的数组 $a 拥有一个字符串 ‘1’ 的索引(可以使用 var_dump(array_keys($a))来证实这一点),而我们使用 $a[1] 和 $a[‘1’] 都是访问数组 $a 中索引为 1 的元素,而 $a 并不存在该元素,因此出现错误 Undefined offset。

小结:PHP 默认不会存储整型字符串的索引,会将其转换为数值,在将对象转换为数组的过程中可能引入整型字符串的索引,如果给出索引为整数或整形字符串,访问数组元素都会去获取数组的对应数值索引。

本文实例完整代码如下:

<?php
$json = '{"1":"foo"}';
$o = json_decode($json);

$a = (array)$o;
print_r($a);
echo $a[1], PHP_EOL;
echo $a['1'], PHP_EOL;

$a2 = json_decode($json, true);
print_r($a2);
echo $a2[1], PHP_EOL;
echo $a2['1'], PHP_EOL;

var_dump($a);
var_dump($a2);

var_dump(array_keys($a));
var_dump(array_keys($a2));

foreach ($a2 as $k => $v) {
	var_dump($k);
	var_dump($v);
}
foreach ($a as $k => $v) {
	var_dump($k);
	var_dump($v);
}

 

各种语言实现函数累加器函数

这两天都在看《黑客与画家》,作者 Paul Graham 是一个推崇 Lisp 为编程能力最强的语言的支持者。第13章,作者各种强调 Lisp 的强大,最后在该章节末尾给出了一个例子来说明各个编程语言的能力是不一样的,Lisp 是最强大的。

原文引入如下:

为了解释我所说的语言编程能力不一样,请考虑下面的问题。我们需要写一个函数,它能够生成累加器,即这个函数接受参数 n,然后返回另一个函数,后者接受参数 i,然后返回 n 增加了 i 后的值。「这里说的是增加,而不是 n 和 i 的相加(plus)。累加器就是应该完成 n 的累加。」

接下来,作者了给出了各种语言实现上述问题的写法:

Common Lisp:

(defun foo (n)
  (lambda (i) (incf n i)))

Scheme:

(define (foo n)
  lambda (i) (set! n (+ n i) n))

Goo:

(df foo(n) (op incf n _))

Arc:

(def foo (n) [++ n _])

作者把 Scheme, Goo, Arc 这三种语言称作 Lisp 的方言(哈哈哈,大概是想说,这些都是在模仿 Lisp 吧)

Ruby:

def foo (n)
  lambda {|i| n += i} end

Perl:

sub foo {
  my ($n) = @_;
  sub {$n += shift}
}

Smalltalk:

foo: n
  |s|
  s := n.
  ^[:i| s := s+i. ]

Javascript:

function foo(n) {
  return function (i) {
    return n += i; } }

Python:

def foo(n):
    s = [n]
    def bar(i):
        s[0] += i
        return s[0]
    return bar

作者还提到,Python 用户完全可以合理地质疑为什么不能写成下面这样,并且他猜想,Python 有一天会支持这样的写法:

def foo(n):
    return lambda i: return n += i

或者:

def foo(n):
    lambda i: n += i

Python 采用面向对象模拟闭包的实现:

def foo(n):
    class acc:
        def __init__(self, s):
            self.s = s
        def inc(self, i):
            self.s += i
            return self.s
    return acc(n).inc

或者:

class foo:
    def __init__(self, n):
        self.n = n
    def __call__(self, i):
        self.n += i
        return self.n

其他语言根本无法解决这个问题,肯 安得森说,Java 只能写出一个近视的解法:

public interface Inttoint {
  public int call(int i);
}

public static Inttoint foo(final int n) {
  return new Inttoint () {
    int s = n;
    public int call(int i) {
      s = s + i;
      return s;
  }};
}

感觉这个就比较精彩了,我看得懂只有 js、java 和 python,perl 虽然去年看过一本 perl 入门的书,但是现在已经忘干净了。

作者还强调,其他语言无法解决这个问题,这句话并不完全正确。因为所有编程语言都是图灵等价的,意味着可以使用任何一种语言写出任何一个程序。只不过不同语言实现起来难度和复杂度不一样而已。

Lisp 的那种写法,看得我好别扭,还是看 js 最舒服。

解决 CENTOS ssh 连接上后乱码问题

1、更改 /etc/sysconfig/i18n 里的 LANG 为 “en_US.UTF-8″;

2、更改 /etc/profile 里的 LANG 为 “en_US.UTF-8″;

3、更改 /home/YOUR_NAME/.bashrc 里的 LANG(如果有该项就更改,没有不用管) 为 “en_US.UTF-8″;

其实解决乱码问题很简单,只要保持各个地方编码方式一致(当然,上面你要改为 “zh_CN.GB18030” 也是可以的)就 OK 了,依次检查系统语言设置有关的配置文件,都更改为想要的编码方式(这里主要是为了防止用户个人设置被修改,其实只需要在 .bashrc 里面指定编码就OK了,因为个人设置会覆盖系统的默认设置),然后使用客户端 putty 或者 SecureCRT 更改编码方式和上面设置保持一致,就不会出问题了。

微调了博客主题

在原主题的基础上适当做了一些修改。

还是两栏布局,目前,左边文章主题部分随着窗口大小自动调整,右边边栏宽度固定。这就是我要的效果。

还有若干小问题:1、边栏和主体中间要隔一定距离才好看一些,但是目前没找到好的方法如何写 css。2、之前的 rss订阅 按钮删除了,因为没调合适,这个按钮加在哪里合适呢。

PHP 支持汉字的反转字符串函数

PHP 里面有一个自带的函数 strrev,该函数可以将字符串反转,例如:

$str = 'abcdef';
echo strrev($str);

将输出:

fedcba

但是该函数并不支持中文,如果字符串含有中文,那么汉字将会乱码。

写了一个支持反转包括汉字的字符串反转函数:

function mb_strrev($str) {
	$len = mb_strlen($str, 'UTF-8');
	$arr = array();
	for ($i = 0; $i < $len; $i++) {
		$arr[] = mb_substr($str, $i, 1, 'UTF-8');
	}
	return implode('', array_reverse($arr));
}

 

使用示例:

$str = '记者获some-letters-here悉嫦娥二号发射工作准备全部就绪';
echo mb_strrev($str);

MySQL JOIN 用法示例详解

本文将图文并茂的讲解 MySQL 的查询时 JOIN 的用法。这里用到的两个示例表及示例数据如下:

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `email` varchar(255) NOT NULL,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `users_email_unique` (`email`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user` VALUES (1,'xiaoming@qq.com','小明'),(2,'xiaowang@qq.com','小王'),(6,'xiangzhang@qq.com','小张'),(7,'xiaoli@qq.com','小李');

CREATE TABLE `user_info` (
  `user_id` int(10) NOT NULL,
  `age` int(10) NOT NULL,
  `name` varchar(255) NOT NULL,
  PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `user_info` VALUES (1,22,'小明'),(2,21,'小王'),(3,33,'小郑');

插入数据库后数据如下:

user_datauser_info_data

MySQL 官网上介绍 JOIN 一共有以下几种形式,接下来笔者将一一介绍:

table_reference [INNER | CROSS] JOIN table_factor [join_condition]
  | table_reference STRAIGHT_JOIN table_factor
  | table_reference STRAIGHT_JOIN table_factor ON conditional_expr
  | table_reference {LEFT|RIGHT} [OUTER] JOIN table_reference join_condition
  | table_reference NATURAL [{LEFT|RIGHT} [OUTER]] JOIN table_factor

JOIN、CROSS JOIN、INNER JOIN

MySQL 官网说:In MySQL, JOIN, CROSS JOIN, and INNER JOIN are syntactic equivalents (they can replace each other). JOIN, CROSS JOIN, 和 INNER JOIN 是等价的,可以相互替换。下面的例子不再出现 CROSS JOIN 和 INNER JOIN。

直接使用 JOIN 连接两个表查询得到结果为:

snapshot1

可以看到,两表结果进行了笛卡尔积,最终查出 12 行记录。这是没带任何条件的查询,这条语句等价于:

SELECT * FROM user, user_info

MySQL 官网如是说:

INNER JOIN and , (comma) are semantically equivalent in the absence of a join condition: both produce a Cartesian product between the specified tables (that is, each and every row in the first table is joined to each and every row in the second table).

说的是,在没有条件的情况下,INNER JOIN 和 ,(逗号)是等价的,都返回两张表的笛卡尔积。
我们来试试带条件的 JOIN 查询:

snapshot2
这个结果很清晰,连接的条件是 user.id=user_info.user_id。结果返回两行记录。

USING

我们来看看 USING 的用法:

snapshot3

USING(name) 等价于 ON user.name=user_info.name,但是大家请注意,这里有一点点区别,使用 USING 得到的结果只有一个 name 字段,且 name 字段在结果最前面,而使用 ON 得到的结果有两个 name 字段。

我们再试试使用 USING 去查并不是两张表都有的字段:

snapshot4

提示错误,因为 user_info 表里并没有 id 字段。

JOIN 与 LEFT JOIN 和 RIGHT JOIN

snapshot6

在 LEFT JOIN 里面,如果右边的表里没有匹配左边表的记录,则右边表所有字段为 NULL;同理,在 RIGHT JOIN 里面,如果左边的表里没有匹配右边表的记录,则左边表所有字段为 NULL;而 JOIN 只查询出两张表都存在的数据。

利用 LEFT JOIN 可以查出左边表存在而右边表不存在的记录,如下图:

snapshot7

STRAIGHT_JOIN

STRAIGHT_JOIN 和 JOIN 一样,除了前者可以保证左边的表先查。STRAIGHT_JOIN 可以用在防止 JOIN 优化器将表的顺序搞错。

NATURAL

NATURAL JOIN 等价于使用 USING,它会 USING 所有两张表里都包含的字段:

snapshot8

OUTER

至于这货,笔者也没搞明白是干什么的。官网给出的实例讲的是用在 ODBC 里,笔者没看明白。

上面所有的示例都只演示了两张表 JOIN,可不要以为只能两张表进行 JOIN 查询。一下是一些 SQL 示例:

SELECT * FROM t1, t2, t3;
SELECT * FROM t1 LEFT JOIN (t2 CROSS JOIN t3 CROSS JOIN t4)
                 ON (t2.a=t1.a AND t3.b=t1.b AND t4.c=t1.c);

ps:大家有没有发现 wordpress 写作时,编辑器用得很不爽啊,比如,插入代码后想回到正常编辑必须切换到文本模式下,在 </pre> 标签后先打几个字符然后在回可视化模式正常写作。还有,每次发文章都要跑到后台来,没有直接用 markdown 舒服啊,直接本地写好,提交上去,多方便。

PHP 实现单例模式的两种方式

第一种方式是由类维护一个静态属性,该属性是对象实例的引用,示例代码如下:

class Singleton {
        private static $_instance = null;
        public static function getInstance() {
                is_null(self::$_instance) && self::$_instance = new self();
                return self::$_instance;
        }
        private function __construct() { // 构造函数 private,防止类在外部被 new 出来
                ;
        }
}

 

第二种方式是由静态方法里面的一个静态变量返回对象实例的引用,示例代码如下:

class Singleton {
        public static function getInstance() {
                static $_instance = null;
                is_null($_instance) && $_instance = new self();
                return $_instance;
        }
        private function __construct() { // 构造函数 private,防止类在外部被 new 出来
                ;
        }
}

 

这两种实现有什么区别呢?效果是一样的吧~

今天才知道原来 PHP 5.3.0 之前版本是不支持延迟绑定的,赶紧把前两天写的类改过来,因为公司线上环境还是用的 5.2 系列版本。关于 PHP 延迟绑定(官网称呼其为:后期静态绑定)请看官网说明:http://www.php.net/manual/zh/language.oop5.late-static-bindings.php

一个更改 hosts 的 PHP 脚本

有这样一个需求,我有多个网址希望在不同的时候对应不同的 ip,如果一个个配 hosts,这工作显得有些繁琐。写了如下脚本来批量更改。

<?php

define('HOST_FILE', 'C:\Windows\System32\drivers\etc\hosts');

$hm = new HostManage(HOST_FILE);

$env = $argv[1];
if (empty($env)) {
        $hm->delAllGroup();
} else {
        $hm->addGroup($env);
}

class HostManage {

        // hosts 文件路径
        protected $file;
        // hosts 记录数组
        protected $hosts = array();
        // 配置文件路径,默认为 __FILE__ . '.ini';
        protected $configFile;
        // 从 ini 配置文件读取出来的配置数组
        protected $config = array();
        // 配置文件里面需要配置的域名
        protected $domain = array();
        // 配置文件获取的 ip 数据
        protected $ip = array();

        public function __construct($file, $config_file = null) {
                $this->file = $file;
                if ($config_file) {
                    $this->configFile = $config_file;
                } else {
                    $this->configFile = __FILE__ . '.ini';
                }
                $this->initHosts()
                        ->initCfg();
        }

        public function __destruct() {
                $this->write();
        }

        public function initHosts() {
                $lines = file($this->file);
                foreach ($lines as $line) {
                        $line = trim($line);
                        if (empty($line) || $line[0] == '#') {
                                continue;
                        }
                        $item = preg_split('/\s+/', $line);
                        $this->hosts[$item[1]] = $item[0];
                }
                return $this;
        }

        public function initCfg() {
                if (! file_exists($this->configFile)) {
                        $this->config = array();
                } else {
                        $this->config = (parse_ini_file($this->configFile, true));
                }
                $this->domain = array_keys($this->config['domain']);
                $this->ip = $this->config['ip'];
                return $this;
        }

        /**
         * 删除配置文件里域的 hosts 
         */
        public function delAllGroup() {
                foreach ($this->domain as $domain) {
                        $this->delRecord($domain);
                }
        }

        /**
         * 将域配置为指定 ip
         * @param type $env
         * @return \HostManage
         */
        public function addGroup($env) {
                if (! isset($this->ip[$env])) {
                        return $this;
                }
                foreach ($this->domain as $domain) {
                        $this->addRecord($domain, $this->ip[$env]);
                }
                return $this;
        }

        /**
         * 添加一条 host 记录
         * @param type $ip
         * @param type $domain
         */
        function addRecord($domain, $ip) {
                $this->hosts[$domain] = $ip;
                return $this;
        }

        /**
         * 删除一条 host 记录
         * @param type $domain
         */
        function delRecord($domain) {
                unset($this->hosts[$domain]);
                return $this;
        }

        /**
         * 写入 host 文件
         */
        public function write() {
                $str = '';
                foreach ($this->hosts as $domain => $ip) {
                        $str .= $ip . "\t" . $domain . PHP_EOL;
                }
                file_put_contents($this->file, $str);
                return $this;
        }

}

示例配置文件如下:

# 域名
[domain]
a.example.com=1 # 请无视这个 =1,因为使用了 parse_ini_file 这个函数来解析,如果后面不带值,就获取不到这条记录了
b.example.com=1
c.example.com=1

# ip 记录
[ip]
local=127.0.0.1
dev=192.168.1.100

使用方法:

php hosts.php local # 域名将指向本机 127.0.0.1
php hosts.php dev # 域名将指向开发机 192.168.1.100
php hosts.php # 删除域名的 hosts 配置

写完后,发现,这明明就是只需要一次查找替换就能完成的工作嘛

小心 foreach 中使用引用,否则可能数据出错

有实例代码如下:

// 有一个产品分为标准版和高级版
$products = [
	'standard' => [
		'price' => 100, // 原价
		'discount' => 0.8, // 折扣
	],
	'advanced' => [
		'price' => 200,
		'discount' => 0.7,
	],
]; 

// 算出节省的金额
foreach ($products as &$product) {
	$product['save'] = $product['price'] * (1 - $product['discount']);
}

// 输出产品价格信息
foreach ($products as $product) {
	print_r($product);
}

 

运行,输出的结果却是:

$ php test_foreach.php
Array
(
    [price] => 100
    [discount] => 0.8
    [save] => 20
)
Array
(
    [price] => 100
    [discount] => 0.8
    [save] => 20
)

 

不知道看官您看出问题没有,找这个问题花了我老长时间,foreach 里面对值使用使用后,loop 结束,值还存在,所以在 foreach 循环结束后应手动 unset 该变量。

改正后的 PHP 代码为:

$products = [
	'standard' => [
		'price' => 100,
		'discount' => 0.8,
	],
	'advanced' => [
		'price' => 200,
		'discount' => 0.7,
	],
]; 

foreach ($products as &$product) {
	$product['save'] = $product['price'] * (1 - $product['discount']);
}
unset($product); // ******** <--注意这里!!! **********

foreach ($products as $product) {
	print_r($product);
}

 

php.net 上关于 foreach 里面使用引用的警告说明如下:

Reference of a $value and the last array element remain even after the foreach loop. It is recommended to destroy it by unset().

新置 vps

前两天将博客放在 SAE 上,速度实在是感觉太慢了。昨晚准备写文一篇时,后台编辑文章竟然都加载不出来,于是购买了一年的 vps,199 元每年,256M 内存,感觉速度还不错。

选择系统的时候安装的是 centos5.5,源里自带的 php mysql,我觉得版本太低,于是,下载源码编译安装,折腾了 2 个小时,最终还是没搞定。编译安装 nginx 倒是很简单。第一次编译 php 时,默认 ./configure 没带任何参数,成功编译安装完成,后想到,需要 enable-fpm 选项啊,于是重新编译。再一次编译的时候 enable 其它一些东西,就出现一些库和依赖,搞烦我了。而下载 mysql 也启动不起来。最终换了 ubuntu 12.04 系统。ubuntu 多爽,一条命令装好所有需要的东西:

sudo apt-get install mysql-server nginx php5 php5-fpm php5-cli php5-gd php5-mcrypt php5-curl

接下来就是配置 nginx 了。先将博客所有文件复制过来,数据库导入,为了方便,网站根目录我放在了 /root/vhosts/upiu.net 下,访问,提示没权限。将 upliu.net 文件夹所有者改为 www-data 问题依旧。再将 upliu.net 权限改为 777 ,还是提示没权限。G 上搜索了一会,后来终于找到问题所在,upliu.net 文件夹权限设置没有问题,但是 www-data 用户没有访问上一层文件夹的权限,/root 和 /root/vhosts 这两个文件夹所有者为 root,www-data 当然无法读取了。

您觉得新主机速度怎么样?

update:2013-11-08 15:27:24 >>>>>>>>>>>

发现如果多刷新几下博客,就会响应很慢很慢,甚至出现 504 gateway time out 错误,php5-fpm 占用内存过大,导致系统反应慢,就连 ssh 都几乎没有响应。将 php5-fpm service 重启一下就好了。暂时解决办法:加了个定时任务,每隔一小时重启一下 php5-fpm 服务。