魔术方法是以两个下画线“_”开头、具有特殊作用的一些方法,可以看做PHP的“语法糖”。
语法糖指那些没有给计算机语言添加新功能,而只是对人类来说更“甜蜜”的语法。语法糖往往给程序员提供了更实用的编码方式或者一些技巧性的用法,有益于更好的编码风格,使代码更易读。不过其并没有给语言添加什么新东西。PHP里的引用、SPL等都属于语法糖。
实际上,在1.1节代码中就涉及魔术方法的使用。family类中的_construct方法就是一个标准魔术方法。这个魔术方法又称构造方法。具有构造方法的类会在每次创建对象时先调用此方法,所以非常适合在使用对象之前做一些初始化工作。因此,这个方法往往用于类进行初始化时执行一些初始化操作,如给属性赋值、连接数据库等。
以代码清单1-1所示代码为例,family中的_construct方法主要做的事情就是在创建对象的同时对属性赋值。也可以这么使用:
$tom=new family($student,'peking');
$tom->people->say();
这样做就不需要在创建对象后再去赋值了。有构造方法就有对应的析构方法,即_destruct方法,析构方法会在某个对象的所有引用都被删除,或者当对象被显式销毁时执行。这两个方法是常见也是最有用的魔术方法。
_set和_get方法
_set和_get是两个比较重要的魔术方法,如代码清单所示。
代码清单 magic.php
<?php
class Account{
private$user=1;
private$pwd=2;
}
$a=new Account();
echo$a->user;
$a->name=5;
echo$a->name;
echo$a->big;
运行这段代码会怎样呢?结果报错如下:
Fatal error:Cannot access private property Account::$user in G:\bak\temp\tempcode\_sg.php on line 7
所报错误大致是说,不能访问Account对象的私有属性user。在代码清单类定义里增加以下代码,其中使用了_set魔术方法。
public function _set($name,$value){
echo "Setting$name to$value\r\n";
$this->$name=$value;
}
public function _get($name){
if(!isset($this->$name)){
echo'未设置';
$this->$name="正在为你设置默认值";
}
return$this->$name;
}
再次运行,看到正常输出,没有报错。在类里以两个下画线开头的方法都属于魔术方法(除非是你自定义的),它们是PHP中的内置方法,有特殊含义。手册里把这两个方法归到重载。
PHP的重载和Java等语言的重载不同。Java里,重载指一个类中可以定义参数列表不同但名字相同的多个方法。比如,Java也有构造函数,Java允许有多个构造函数,只要保证方法签名不一样就行;而PHP则在一个类中只允许有一个构造函数。
PHP提供的“重载”指动态地“创建”类属性和方法。因此,_set和_get方法被归到重载里。
这里可以直观看到,若类中定义了_set和_get这一对魔术方法,那么当给对象属性赋值或者取值时,即使这个属性不存在,也不会报错,一定程度上增强了程序的健壮性。
我们注意到,在account类里,user属性的访问权限是私有的,私有属性意味着这个属性是类的“私有财产”,只能在类内部对其进行操作。如果没有_set这个魔术方法,直接在类的外部对属性进行赋值操作是会报错的,只能通过在类中定义一个public的方法,然后在类外调用这个公开的方法进行属性读写操作。
现在有了这两个魔术方法,是不是对私有属性的操作变得更方便了呢?实际上,并没有什么奇怪的,因为这两个方法本身就是public的。它们和在对外的public方法中操作private属性的原理一样。只不过这对魔术方法使其操作更简单,不需要显式地调用一个public的方法,因为这对魔术方法在操作类变量时是自动调用的。当然,也可以把类属性定义成public的,这样就可以随意在类的外部进行读写。不过,如果只是为了方便,类属性在任意时候都定义成public权限显然是不合适的,也不符合面向对象的设计思想。
_call和_callStatic方法
如何防止调用不存在的方法而出错?一样的道理,使用_call魔术重载方法。
_call方法原型如下:
mixed _call(string$name,array$arguments)
当调用一个不可访问的方法(如未定义,或者不可见)时,_call()会被调用。其中$name参数是要调用的方法名称。$arguments参数是一个数组,包含着要传递给方法的参数,如下所示:
public function _call($name,$arguments){
switch(count($arguments)){
case 2:
echo $arguments[0]*$arguments[1],PHP_EOL;
break;
case 3:
echo array_sum($arguments),PHP_EOL;
break;
default:
echo '参数不对',PHP_EOL;
break;
}
}
$a->make(5);
$a->make(5,6);
以上代码模拟了类似其他语言中的根据参数类型进行重载。跟_call方法配套的魔术方法是_callStatic。当然,使用魔术方法“防止调用不存在的方法而报错”,并不是魔术方法的本意。实际上,魔术方法使方法的动态创建变为可能,这在MVC等框架设计中是很有用的语法。假设一个控制器调用了不存在的方法,那么只要定义了_call魔术方法,就能友好地处理这种情况。
试着理解代码清单1-3所示代码。这段代码通过使用_callStatic这一魔术方法进行方法的动态创建和延迟绑定,实现一个简单的ORM模型。
代码清单1-3 simpleOrm.php
<?php
abstract class ActiveRecord{
protected static $table;
protected $fieldvalues;
public $select;
static function findById($id){
$query = "select*from"
.static::$table
."where id=$id";
return self::createDomain($query);
}
function _get($fieldname){
return $this->fieldvalues[$fieldname];
}
static function _callStatic($method,$args){
$field = preg_replace('/^findBy(\w*)$/','${1}',$method);
$query = "select*from"
.static::$table
."where$field = '$args[0]'";
return self::createDomain($query);
}
private static function createDomain($query){
$klass = get_called_class();
$domain = new$klass();
$domain->fieldvalues = array();
$domain->select = $query;
foreach($klass::$fields as$field=>$type){
$domain->fieldvalues[$field]='TODO:set from sql result';
}
return $domain;
}
}
class Customer extends ActiveRecord{
protected static $table='custdb';
protected static $fields=array(
'id'=>'int',
'email'=>'varchar',
'lastname'=>'varchar'
);
}
class Sales extends ActiveRecord{
protected static $table='salesdb';
protected static $fields=array(
'id'=>'int',
'item'=>'varchar',
'qty'=>'int'
);
}
assert("select*from custdb where id=123"==
Customer::findById(123)->select);
assert("TODO:set from sql result"==
Customer::findById(123)->email);
assert("select*from salesdb where id=321"==
Sales::findById(321)->select);
assert("select*from custdb where Lastname='Denoncourt'"==
Customer::findByLastname('Denoncourt')->select);
再举个类似的例子。PHP里有很多字符串函数,假如要先过滤字符串首尾的空格,再求出字符串的长度,一般会这么写:
strlen(trim($str));
如果要实现JS里的链式操作,比如像下面这样,应该怎么实现?
很简单,先实现一个String类,对这个类的对象调用方法进行处理时,触发_call魔术方法,接着执行call_user_func即可。
_toString方法
再看另外一个魔术方法_TOstring(在这里故意这么写,是要说明PHP中方法不区分大小写,但实际开发中还需要注意规范)。
当进行测试时,需要知道是否得出正确的数据。比如打印一个对象时,看看这个对象都有哪些属性,其值是什么,如果类定义了_toString方法,就能在测试时,echo打印对象体,对象就会自动调用它所属类定义的_toString方法,格式化输出这个对象所包含的数据。如果没有这个方法,那么echo一个对象将报错,例如"Catchable fatal error:Object of class Account could not be converted to string"语法错误,实际上这是一个类型匹配失败错误。不过仍然可以用print_r()和var_dump()函数输出一个对象。当然,_toString是可以定制的,所提供的信息和样式更丰富,如代码清单1-4所示。
代码清单1-4 magic_2.php
<?php
class Account{
public $user = 1;
private $pwd = 2;
//自定义的格式化输出方法
public function_toString(){
return"当前对象的用户名是{$this->user},密码是{$this->pwd}";
}
}
$a = new Account();
echo$a;
echo PHP_EOL;
print_r($a);
运行这段代码发现,使用_toString方法后,输出的结果是可定制的,更易于理解。实际上,PHP的_toString魔术方法的设计原型来源于Java。Java中也有这么一个方法,而且在Java中,这个方法被大量使用,对于调试程序比较方便。实际上,_toString方法也是一种序列化,我们知道PHP自带serialize/unserialize也是进行序列化的,但是这组函数序列化时会产生一些无用信息,如属性字符串长度,造成存储空间的无谓浪费。因此,可以实现自己的序列化和反序列化方法,或者json_encode/json_decode也是一个不错的选择。
为什么直接echo一个对象就会报语法错误,而如果这个对象实现_toString方法后就可以直接输出呢?原因很简单,echo本来可以打印一个对象,而且也实现了这个接口,但是PHP对其做了个限制,只有实现_toString后才允许使用。从下面的PHP源代码里可以得到验证:
ZEND_VM_HANDLER(40,ZEND_ECHO,CONST|TMP|VAR|CV,ANY)
{
zend_op*opline = EX(opline);
zend_free_op free_op1;
zval z_copy;
zval *z =GET_OP1_ZVAL_PTR(BP_VAR_R);
//此处的代码预留了把对象转换为字符串的接口
if(OP1_TYPE ! = IS_CONST&&
Z_TYPE_P(z) == IS_OBJECT&&Z_OBJ_HT_P(z)->get_method ! = NULL&&
zend_std_cast_object_tostring(z,&z_copy,IS_STRING TSRMLS_CC) == SUCCESS){
zend_print_variable(&z_copy);
zval_dtor(&z_copy);
}else{
zend_print_variable(z);
}
FREE_OP1();
ZEND_VM_NEXT_OPCODE();
}
由此可见,魔术方法并不神奇。
有比较才有认知。最后,针对本节代码给出一个Java版本的代码,供各位读者用来对比两种语言中重载和魔术方法的异同,如代码清单1-5所示。
代码清单1-5 Account.java
import org.apache.commons.lang3.builder.ToStringBuilder;
/**
*类的重载演示Java版本
*@author wfox
*@date@verson
*/
public class Account{
private String user;//用户名
private String pwd;//密码
public Account(){
System.out.println("构造函数");
}
public Account(String user,String pwd){
System.out.println("重载构造函数");
System.out.println(user+"---"+pwd);
}
public void say(String user){
System.out.println("用户是:"+user);
}
public void say(String user,String pwd){
System.out.println("用户:"+user);
System.out.println("密码"+pwd);
}
public String getUser(){
return user;
}
public void setUser(String user){
this.user = user;
}
public String getPwd(){
return pwd;
}
public void setPwd(String pwd){
}
@Override
public String toString(){
return ToStringBuilder.reflectionToString(this);
}
public static void main(String……){
Account account = new Account();
account.setUser("张三");
account.setPwd("123456");
account.say("李四");
account.say("王五","123");
System.out.println(account);
}
}
可以看出,Java的构造方法比PHP好用,PHP由于有了_set/_get这一对魔术方法,使得动态增加对象的属性字段变得很方便,而对Java来说,要实现类似的效果,就不得不借助反射API或直接修改编译后字节码的方式来实现。这体现了动态语言的优势,简单、灵活。
上一篇: 在网站制作中各种语言中的多态
下一篇: 网站设计中的说服力
售后保障
承诺任何问题1小时内解决数据备份
更安全、更高效、更稳定价格公道精准
项目经理精准报价不弄虚作假合作无风险
重合同讲信誉,无效全额退款