标签 PHP 下的文章

[译]开始使用领域驱动设计 - 《Domain-Driven Design in PHP》第1章

本篇博文由本博客(http://www.veitor.net)经原文翻译,转载请注明出处。

有什么大惊小怪的呢?如果你已经阅读了Vaughn Vernon和Eric Evans关于这个话题的书籍,那么你可能对我们即将要谈论的会很熟悉,因为我们大量借鉴了他们书中的定义和解释。 领域驱动设计(DDD)是帮助我们成功理解和构建软件模型设计的一种方法。 它为我们提供了策略(strategic)和战术(tactical)建模工具,以帮助设计符合我们业务目标的高质量软件。

- 阅读剩余部分 -

设计模式笔记:第二个设计模式原则——优先选择组合而非继承

QQ截图20151223224240.jpg

有些OOP程序员认为对象重用、扩展就等同于使用继承。一个类可以有大量属性和方法,通过继承这个类,我们增加新属性和方法,就能轻松的进行扩展,因为无需再重新编写代码。不过最后对于紧密绑定的对象,一味的使用继承方式来扩展会导致这么一个问题,那就是过度继承

- 阅读剩余部分 -

Yii事件机制分析

在Yii中使用事件需要三个步骤:1、定义事件;2、定义事件回调函数;3、将回调函数添加到事件中4、触发事件。

Yii事件机制的实现是在其底层基类CComponent类里,这是所有组件的基类。

1、如何定义事件

很简单,事件以on开头的命名方式定义,如以下定义了一个onEcho的事件:

public function onEcho($event)
{
     $this->raiseEvent('onEcho', $event);
}

这样就定义了一个事件,其中$event参数是一个CEvent或其子类的实例(但其实用到的并不多,下面再说)。

2、定义事件回调函数

回调函数可以是一个全局函数或者一个类中的函数(说白了就是一个函数而已),其需要一个参数$event,如下:

public function huidiao($event)
{
     //TODO
}

$event参数其实是接收定义事件时传入的那个参数,也就是上面第一步定义的事件函数的参数,它最终会传到这个回调函数里来。然后回调函数根据业务逻辑使用这个参数,但一般情况下我们不怎么使用到这个参数,所以意义也不是很大,不过如果你需要参数的情况下可以通过这样方式定义,因为传入的参数并不一定限制是CEvent的实例或其子类,也可以是其他类型(待会下面继续解释)。

3、将回调函数添加到事件中

为啥还要将回调函数添加到事件中呢?因为当事件触发后总得要有程序逻辑(也就是一段php代码)去处理业务是吧。所以光光的定义一个事件而不往里面添加回调函数是没有意义的,而且即使定义了回调函数却添加到事件里也是没有意义的。就如同你只买了个电脑主机有啥意义呢?也或者你只买了一台显示器也同样没有意义,要将两者结合才是他们真正的用处。

那么如何将回调函数添加到事件中?

一种方法:$this->onEcho=array($this, 'huidiao');
另一种方法:$this->attachEventHandler('onEcho', array($this, 'huidiao'));
第一种方法利用了Yii组件中setter原理,看代码:

public function __set($name,$value)
{
	$setter='set'.$name;
	if(method_exists($this,$setter))
		return $this->$setter($value);
	elseif(strncasecmp($name,'on',2)===0 && method_exists($this,$name))
	{
		// duplicating getEventHandlers() here for performance
		$name=strtolower($name);
		if(!isset($this->_e[$name]))
			$this->_e[$name]=new CList;
		return $this->_e[$name]->add($value);
	}
	elseif(is_array($this->_m))
	{
		foreach($this->_m as $object)
		{
			if($object->getEnabled() && (property_exists($object,$name) || $object->canSetProperty($name)))
				return $object->$name=$value;
		}
	}
	if(method_exists($this,'get'.$name))
		throw new CException(Yii::t('yii','Property "{class}.{property}" is read only.',
			array('{class}'=>get_class($this), '{property}'=>$name)));
	else
		throw new CException(Yii::t('yii','Property "{class}.{property}" is not defined.',
			array('{class}'=>get_class($this), '{property}'=>$name)));
}

这是一个魔术方法__set,6-13行是给事件添加回调函数的步骤。如果类里面存在以on开头的函数,那么将事件放到私有变量$_e里,$_e是个数组,其键名是事件名称,其值是一个CList实例(也可以理解为一个数组,因为CList实现了一个整数索引的集合类,可以用数组的形式往里面添加元素 ,可详见CList),再使用CList中add方法往数组形式的值里面添加回调函数,也等同于$_e['onecho'][]=array($this,'huidiao')。那么按我上面添加一个事件来说,添加一个回调函数huidiao后$_e的内容应该是array('onecho'=>array(array($this,'huidiao'))

第二种方法使用了类中的attachEventHandler函数,代码如下:

public function attachEventHandler($name,$handler)
{
	$this->getEventHandlers($name)->add($handler);
}

public function getEventHandlers($name)
{
	if($this->hasEvent($name))
	{
		$name=strtolower($name);
		if(!isset($this->_e[$name]))
			$this->_e[$name]=new CList;
		return $this->_e[$name];
	}
	else
		throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.',
			array('{class}'=>get_class($this), '{event}'=>$name)));
}

public function hasEvent($name)
{
	return !strncasecmp($name,'on',2) && method_exists($this,$name);
}

attachEventHandler函数首先用getEventHandler函数获取需要添加回调函数的事件,用hasEvent判断是否存在这个事件函数名,如果存在的话(也就是定义事件函数的话)就和第一种方法原理一样,获得$_e['onecho']的值(CList实例),使用其方法add往数组形式的值里添加回调函数。

至此,回调函数都已经添加到事件中去了。

4、触发事件

这个比较简单,在你需要执行这个事件的地方调用第1步定义的事件,如$this->onEcho($event),函数执行里面的一句话$this->raiseEvent('onEcho'.$event),raiseEvent这个方法代码如下:

public function raiseEvent($name,$event)
{
	$name=strtolower($name);
	if(isset($this->_e[$name]))
	{
		foreach($this->_e[$name] as $handler)
		{
			if(is_string($handler))
				call_user_func($handler,$event);
			elseif(is_callable($handler,true))
			{
				if(is_array($handler))
				{
					// an array: 0 - object, 1 - method name
					list($object,$method)=$handler;
					if(is_string($object))	// static method call
						call_user_func($handler,$event);
					elseif(method_exists($object,$method))
						$object->$method($event);
					else
						throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
							array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>$handler[1])));
				}
				else // PHP 5.3: anonymous function
					call_user_func($handler,$event);
			}
			else
				throw new CException(Yii::t('yii','Event "{class}.{event}" is attached with an invalid handler "{handler}".',
					array('{class}'=>get_class($this), '{event}'=>$name, '{handler}'=>gettype($handler))));
			// stop further handling if param.handled is set true
			if(($event instanceof CEvent) && $event->handled)
				return;
		}
	}
	elseif(YII_DEBUG && !$this->hasEvent($name))
		throw new CException(Yii::t('yii','Event "{class}.{event}" is not defined.',
			array('{class}'=>get_class($this), '{event}'=>$name)));
}

这个方法会获得$_e['onecho']值,那么这个值是数组形式对吧,而且数组的每个元素都是一个回调函数,那么用foreach对其进行遍历(代码第6行),循环部分的代码最终都是调用了call_user_func,把回调函数和$event传入进去,那么回调函数就被执行了,并且该回调函数得到的参数是触发事件时$this->onEcho($event)的这个$event参数。所以我在第2步中说道,这个$event参数并不一定要CEvent的实例或其子类,因为要看你回调函数需要什么参数。这么一看,通过foreach后,注册到这个事件onEcho中的所有回调函数都会被执行一遍。

那么就这样完成了整个事件了,还比较容易理解吧?有任何疑问可以留言,一起探讨

 

PHP命名大小写敏感规则

一直觉得PHP中各种大小写规则理不清,就连工作多年的老手们也不一定能对PHP大小写敏感问题足够了解。在PHP中,大小写敏感问题的处理比较乱,大家一定要注意。即使某些地方大小写不敏感,但在编程过程中能始终坚持“大小写敏感”是最好不过的。下面整理了一些大小写问题注意点:

大小写敏感

1. 变量名区分大小写
所有变量均区分大小写,包括普通变量以 及$_GET,$_POST,$_REQUEST,$_COOKIE,$_SESSION,$GLOBALS,$_SERVER,$_FILES,$_ENV 等;

<?php
$abc = 'abc';
echo $abc;    //输出'abc'
echo $aBc;    //无输出
echo $ABC;    //无输出
?>

2、常量名区分大小写

使用define定义的常量是区分大小写的。

<?php
define('BLOGGER','Veitor');
echo BLOGGER;    //输出'Veitor'
echo BLOgger;    //报NOTICE提示,并输出'BLOgger'
echo blogger;    //报NOTICE提示,并输出'blogger'
?>

3、数组索引(键名)区分大小写

<?php
$arr = array('one'=>'first');
echo $arr['one'];    //输出'first'
echo $arr['One'];    //无输出并报错
echo $Arr['one'];    //上面讲过,变量名区分大小写,所以无输出并报错
?>

 

大小写不敏感

1. 函数名、方法名、类名不区分大小写

虽然这些不区分大小写,但坚持“大小写敏感”原则,建议还是使用与定义时相同大小写的名字

<?php
class Test
{
    static public function Ceshi()
    {
        echo '123';
    }

    public funcion Dxx()
    {
        echo '321';
    }
}

$obj = new Test;
$obj->Dxx();    //成功实例化Test类,并调用Dxx方法输出'321'
$obj->dxx();    //成功实例化Test类,并调用Dxx方法输出'321'
$obj = new test;
$obj->Dxx();    //成功实例化Test类,并调用Dxx方法输出'321'
$obj->dxx();    //成功实例化Test类,并调用Dxx方法输出'321'

Test::Ceshi();    //输出'123'
test::Ceshi();    //输出'123'
Test::ceshi();    //输出'123'
test::ceshi();    //输出'123'
?>

2、魔术常量不区分大小写

一些魔术常量包括:__LINE__、__FILE__、__DIR__、__FUNCTION__、__CLASS__、__METHOD__、 __NAMESPACE__等都不区分大小写。

<?php
echo __LINE__;    //输出2
echo __line__;    //输出3
?>

 3、 NULL、TRUE、FALSE不区分大小写

这个知道的人应该比较多就不举例了。

4、强制类型转换不区分大小写

如这些

(int),(integer) – 转换成整型
(bool),(boolean) – 转换成布尔型
(float),(double),(real) – 转换成浮点型
(string) – 转换成字符串
(array) – 转换成数组
(object) – 转换成对象

一般我们都小写,这个问题不大。

 

总的来说,容易搞不明白的就是变量、常量、类名、方法名和函数名,把这些记住对自己会有帮助的。

关于php中curl慢的问题

今天在自己的服务器上运行程序时需要远程请求一个接口,发现此时程序响应特别慢,平均时间都要十几秒。

最后断定问题出现在了curl地方,使用服务器命令(我的是centos)curl一个域名也很慢,但curl一个IP却能得到及时响应。(因此其实本文标题说是PHP中的问题是不严谨的)

这说明问题可能就出现在了DNS设置上,后来改了一下DNS后发现正常了,在此写文记一下。

配置DNS:

输入命令vi /etc/resolv.conf

设置一下DNS,我这里使用的阿里DNS

nameserver 223.5.5.5
nameserver 223.6.6.6

最后再重启下php和nginx

/etc/init.d/php-fpm restart
/etc/init.d/php-fpm restart

 

关于swfupload插件无法上传文件的问题

因为项目上所使用的flash上传图片插件名叫swfupload,但里面的js写法好像另一个上传插件sapload,所以我也搞不清到底是哪个,但最终了解到这两个插件都是有共同的一个问题,就是对需要进行身份才能上传文件的方式会失效。

我们都知道服务器端要对识别当前用户登录状态需要客户端cookie的支持,请求URL时会带着cookie,服务器端才能找到对应的session。而使用这个flash插件上传文件请求接口时,如果被请求的接口需要登陆才能传文件的话,那很可能就会失败。

一种原因是说:因为该插件会忽略部分浏览器下的cookie,IE和chrome下是正常的,像火狐等浏览器上传文件时,服务器端会发现请求带过来的cookie是空的(建议尝试打印一下cookie)

另一种可能的说法:该插件是通过soket套接字进行通信,相当于建立了一次新对话,即使请求带着cookie过去了,但打印出来发现存储的sessionid并不是当前登录时的,因此用户身份又验证失败。(建议多打印cookie并查看)

归根到底,无法上传文件的原因就是无法验证身份。

解决方法有两种:

1、在初始化flash插件时,在js代码中设置一下传递参数(可参考其插件手册)。swfupload插件中增加一个post_params选项,格式如post_params:{"PHPSESSID": <?php echo session_id(); ?>} ,sapload插件中增加一个args选项,格式如args:"PHPSESSID=<?php echo session_id(); ?>" 。这两个意思都是在post文件时,带上参数PHPSESSID(注:后端是PHP脚本)。

咱用不了cookie中的值,那就用POST过来值,然后在PHP脚本中处理一下:

if(isset($_POST['PHPSESSID'])){
    session_id($_POST['PHPSESSID']);
    session_start()
}

这样就可以使用当前登录正确的session id进行一系列操作了。

2、取消上传URL接口的登陆验证。(有点蛋疼,非必要情况下还是使用第一种方法吧)

 

 

附(这是给我自己看的,swfupload好像格式有所不同,如本文开头提到的,不知道是不是同一个插件):

flash 接口文档:

upUrl :后台文件的地址,必须要绝对路径

etmsg:每次文件上传成功后需不需要回调函数(0|1),默认回调函数为sapLoadMsg(x)函数。

ltmsg:最后一个文件上传成功后需不需要回调函数(0|1),默认回调函数为sapLoadMsg(x)函数。

types:上传文件的类型(如*.apk;*.jpg)用分号分隔各个类型

args:需要传递的参数(如apkName=123;apkName1=1234)用分号分隔各个类型

fileName:此参数可选,允许你自定义文件域名称,默认是Filedata。

maxNum:当最大上传数为1的时候自动切换到单文件上传模式,用户将不能在使用多选选取文件。

pear安装

说到安装pear,我是因为给一个变量起一个名字,联想到了如何起名才规范,于是又联想到了PHPDocument,要安装PHPDocument可以通过pear安装和手动下载安装,于是乎我就折腾起了这个pear的安装。

根据:http://pear.php.net/manual/en/installation.getting.php中的内容翻译得来

点此http://pear.php.net/go-pear.phar下载文件到本地并命名为go-pear.phar进行保存。在cmd中输入php go-pear.phar 开始安装。

Are you installing a system-wide PEAR or a local copy?
(system|local) [system] :

 选择系统级别安装还是安装本地,默认是system,直接按回车继续。

Below is a suggested file layout for your new PEAR installation.  To
change individual locations, type the number in front of the
directory.  Type 'all' to change all of them or simply press Enter to
accept these locations.

 1. Installation base ($prefix)                   : D:\php5
 2. Temporary directory for processing            : D:\php5\tmp
 3. Temporary directory for downloads             : D:\php5\tmp
 4. Binaries directory                            : D:\php5
 5. PHP code directory ($php_dir)                 : D:\php5\pear
 6. Documentation directory                       : D:\php5\docs
 7. Data directory                                : D:\php5\data
 8. User-modifiable configuration files directory : D:\php5\cfg
 9. Public Web Files directory                    : D:\php5\www
10. Tests directory                               : D:\php5\tests
11. Name of configuration file                    : C:\Windows\pear.ini
12. Path to CLI php.exe                           : D:\php5

1-12, 'all' or Enter to continue:

以上是默认的pear的临时、数据、配置、测试、执行目录的设置,第11项默认的话会显示错误,我所以还是改到了D:\php5目录下(输入需要改的数字项,会提示让你输入新的目录地址,不改的话啥也不用输入直接回车)。然后回车。

然后一系列安装,其中有以下这个警告:

WARNING!  The include_path defined in the currently used php.ini does not
contain the PEAR PHP directory you just specified:
<D:\php5\pear>
If the specified directory is also not in the include_path used by
your scripts, you will have problems getting any PEAR packages working.

Would you like to alter php.ini<D:\php5\php.ini>?[Y/n]

需要将pear配置目录D:\php5\pear加入php.ini的include_path指令中。输入Y后自动修改php.ini中的路径。

php.ini <D:\php5\php.ini> include_path updated.

Current include path           : .;C:\php\pear
Configured directory           : D:\php5\pear
Currently used php.ini (guess) : D:\php5\php.ini
Press Enter to continue:

 到这里也没啥了,按回车基本安装成功了。会在D:\php5下面生成一个PEAR_ENV.reg的文件,双击运行进行注册表注册即可。

通过命令pear install package安装程序包,如果出现failed to mkdir的错误,只要以管理员身份重新运行cmd即可。

sql中where 1=1和 0=1 的作用

刚在写sql的时候思考了一下在不确定条件因素时的情况,之前看到别人使用过where 1=1这个条件, 这个条件始终为True,后来了解到在不定数量查询条件情况下,1=1可以很方便的规范语句。

一、不用where  1=1  在多条件查询中的困扰

  举个例子,如果您做查询页面,并且,可查询的选项有多个,同时,还让用户自行选择并输入查询关键词,那么,按平时的查询语句的动态构造,代码大体如下:

$sql=”select * from table where”;

if(!empty($age))
{
    $sql .= 'age='.$age';
}

if(!empty($address))
{
  $sql. = 'and address='.$address;
}

如果上述的两个if判断语句,均为true,即用户都输入了查询词,那么,最终的$sql动态构造语句变为:

$sql= 'select * from table where age=20 and address="常州"';

可以看得出来,这是一条完整的正确的SQL查询语句,能够正确的被执行,并根据数据库是否存在记录,返回数据。

②种假设

如果上述的两个if判断语句不成立,那么,最终的$sql动态构造语句变为:

$sql  = 'select * from table where';

现在,我们来看一下这条语句,由于where关键词后面需要使用条件,但是这条语句根本就不存在条件,所以,该语句就是一条错误的语句,肯定不能被执行,不仅报错,同时还不会查询到任何数据。

上述的两种假设,代表了现实的应用,说明,语句的构造存在问题,不足以应付灵活多变的查询条件。

二、使用 where  1=1  的好处

假如我们将上述的语句改为:

$sql=”select * from table where 1=1”;

if(!empty($age))
{
    $sql .= ' and age='.$age';
}

if(!empty($address))
{
  $sql. = 'and address='.$address;
}

①种假设

如果两个if都成立,那么,语句变为:

$sql = 'select * from table where 1=1 and age=12 and address="常州'",很明显,该语句是一条正确的语句,能够正确执行,如果数据库有记录,肯定会被查询到。

②种假设

如果两个if都不成立,那么,语句变为:

$sql = 'select * from table where 1=1',现在,我们来看这条语句,由于where 1=1 是为true的语句,因此,该条语句语法正确,能够被正确执行,它的作用相当于:$sql = 'select * from table',即返回表中所有数据。

言下之意就是:如果用户在多条件查询页面中,不选择任何字段、不输入任何关键词,那么,必将返回表中所有数据;如果用户在页面中,选择了部分字段并且输入了部分查询关键词,那么,就按用户设置的条件进行查询。

说到这里,不知道您是否已明白,其实,where 1=1的应用,不是什么高级的应用,也不是所谓的智能化的构造,仅仅只是为了满足多条件查询页面中不确定的各种因素而采用的一种构造一条正确能运行的动态SQL语句的一种方法。

where 1=0; 这个条件始终为false,结果不会返回任何数据,只有表结构,可用于快速建表

"select * from table where 1=0"; 该select语句主要用于读取表的结构而不考虑表中的数据,这样节省了内存,因为可以不用保存结果集。

create table newtable as select * from oldtable where 1=0;  创建一个新表,而新表的结构与查询的表的结构是一样的。

PHP性能调试工具xhprof的安装与使用

今天试用了一下facebook的php性能调试工具xhprof,在安装的时候是一波三折,虽说从百度了安装方法,但也折腾了半天,不知是说明没写全还是我个人操作失误,那么我在这把我的安装方法讲述一下。环境是:Linux+Nginx+PHP

#cd /tmp
#mkdir xhprof
#cd xhprof
#wget http://pecl.php.net/get/xhprof-0.9.4.tgz
//解压
#tar zxf xhprof-0.9.4.tgz
#cd xhprof-0.9.4/extension

//拷贝xhprof_html和xhprof_lib两个文件夹至可访问的web目录下,我的web根目录为/home/www
#cp -r xhprof_html xhprof_lib /home/www

//运行phpize
#/usr/local/webserver/php/bin/phpize 

//运行configure为下一步编译做准备,详情了解Linux下的configure命令
#./configure --with-php-config=/usr/local/php/bin/php-config 

#make
#make install

安装完后你会看到一个提示Installing shared extensions /usr/local/php/lib/php/extensions/no-debug-non-zts-20060626/xhprof.so,说明xhprof.so这个模块被生成了

接下来修改php.ini文件,添加:

//添加xhprof.so这个扩展,位置就是刚才生成给你的
extension=/usr/local/php/lib/php/extensions/no-debug-non-zts-20060626/xhprof.so
//指定生成测试报告分析日志的目录,并保证可写权限
xhprof.output_dir=/home/www/tmp

重启php-fpm重新加载php配置文件

#/etc/init.d/php-fpm restart

至此安装完毕,可以搞个phpinfo();页面看看,如果能看到下图则说明安装成功了QQ截图20141103143752

 

xhprof的使用

写一个php脚本如

xhprof_enable();

function test()
{
 return 'this is a test demo';
}
test();

$xhprofData = xhprof_disable();

include_once '/home/www/houseinfo/xhprof_lib/utils/xhprof_lib.php';
include_once '/home/www/houseinfo/xhprof_lib/utils/xhprof_runs.php';

$xhprofRuns = new XHProfRuns_Default();
$xhprofRuns->save_run($xhprofData,'xhprof_foo');

?>

通过URL访问该脚本后,会在之前设定的/home/www/tmp(php.ini中设定的)目录下生成分析日志,如“5457280fd8500.xhprof_foo.xhprof”

其中5457280fd8500为日志生成的id,通过地址http://YOUR_URL/xhprof_html/index.php?run=5457315580b0e&source=xhprof_foo即可查看性能分析(如下图)

QQ截图20141103154222

你可以将上面php代码最后一行改为这样,以便直接点击链接到分析页查看,而无需自己再查看id并拼接成URL访问

<?php
$run_id = $xhprofRuns->save_run($xhprofData,'xhprof_foo');

echo '<a href="/xhprof_html/index.php?run='.$run_id.'&source=xhprof_foo" target="_blank">查看分析报告</a>';
?>

xhprof提供3种报告:

一、单一运行报告:通过http://YOUR_URL/xhprof_html/index.php?run=5457315580b0e&source=xhprof_foo地址查看的单一运行时的报告

二、diff报告:地址如http://YOUR_URL/xhprof_html/index.php?run1=xxxxxx&run2=xxxxxxx&source=xhprof_foo,提供两次对比id即可。

三、汇总报告,指定一组run id来汇总得到您想要的报告视图。如果你有三个XHProf运行,都在"xhprof_foo‘命名空间下,run id分别是1,2,3。要查看这些运行的汇总报告:http://YOUR_URL/xhprof_html/index.php?run=1,2,3&source=xhprof_foo

XHProf输出说明
1. Inclusive Time : 包括子函数所有执行时间。
2. Exclusive Time/Self Time : 函数执行本身花费的时间,不包括子树执行时间。
3. Wall Time : 花去了的时间或挂钟时间。
4. CPU Time : 用户耗的时间+ 内核耗的时间
5. Inclusive CPU : 包括子函数一起所占用的CPU
6. Exclusive CPU : 函数自身所占用的CPU

可是这个界面看起来不是很直观也不爽,我们还可以装一个graphviz画图工具

#wget http://www.graphviz.org/pub/graphviz/stable/SOURCES/graphviz-2.28.0.tar.gz  
#tar -zxvf graphviz-2.28.0.tar.gz  
#cd graphviz-2.28.0  
#./configure  
#make  
#make install

安装完成后,会生成/usr/local/bin/dot文件,确保路径在PATH环境变量里,以便XHProf能找到它,graphviz处于/usr/local/lib/graphviz

#vi ~/.bash_profile

QQ截图20141104133618#echo $PATH 输出一下看看应该有了这个路径

QQ截图20141104135151

之后进入分析页面点击[View Full Callgraph]就能看到类似下图了

callgraph