Bootstrap

【PHP 面向对象】面向对象(OOP)编程之魔术方法实现重载知识点归纳总结(三)

PHP 通过魔术方法实现重载

魔术方法作用
__construct()实例化类时自动调用(构造函数)
__destruct()类对象使用结束时自动调用(析构函数)
__set()在给未定义的属性赋值时自动调用
__get()调用未定义的属性时自动调用
__isset()使用 isset() 或 empty() 函数时自动调用
__unset()使用 unset() 时自动调用
__sleep()使用 serialize 序列化时自动调用
__wakeup()使用 unserialize 反序列化时自动调用
__call()调用一个不存在的方法时自动调用
__callStatic()调用一个不存在的静态方法时自动调用
__toString()把对象转换成字符串时自动调用
__invoke()当尝试把对象当方法调用时自动调用
__set_state()当使用 var_export() 函数时自动调用,接受一个数组参数
__clone()当使用 clone 复制一个对象时自动调用(克隆)
__debugInfo()使用 var_dump() 打印对象信息时自动调用

注意: 这些魔术方法的参数都不能通过引用传递。

  • 在给不可访问属性赋值时,__set() 会被调用。
  • 读取不可访问属性的值时,__get() 会被调用。
  • 当对不可访问属性调用 isset()empty() 时,__isset() 会被调用。
  • 当对不可访问属性调用 unset() 时,__unset() 会被调用。

属性重载(属性拦截器)

PHP所提供的 重载(overloading)是指动态地创建类属性和方法。我们是通过魔术方法(magic methods)来实现的。

当调用当前环境下不可访问(未定义或不可见)的类属性或方法时,重载方法会被调用。

● 属性的重载 __get__set

● 方法的重载 __call__callStatic

<?php  
// PHP 重载
class Credit{
    private $idNum;
    private $age;
    
    // 魔术方法 构造函数 如果我没有显示的定义__construct方法时候,系统在实例化对象的时候会自动初始一个无参的构造函数
    public function __construct($idNum,$age){
        $this->idNum = $idNum;
        $this->age = $age;
    }

    // 我们也可以显示的声明重载方法 __get __set
    public function __set($name,$value){
    	echo "Setting $name to $value<br>";
    	$this->$name = $value;
    	return $this->$name;
    }
    public function __get($name){
    	return $this->$name;
    }
}

$c = new Credit('341226186915868525',88);
// 访问未定义的成员属性时,重载方法会被调用
$c->name = 'zhang'; // __set
echo $c->name; // __get
// 访问私有成员属性
echo "<br>";
$c->idNum = "****************";
echo $c->idNum;
?>

导出问题:因为魔术方法都是公有的,所以一些私有成员的不可见性就不会生效。

解决这一问题,我们需要用到属性重载处理(属性拦截器)

  • __set()当给不可访问的属性赋值的时候会触发

    有两个参数第一个参数是成员名第二个参数是成员的值

  • __get()当读取不可访问的属性的时候会触发
    有一个参数是成员名

// 其中不可访问成员指的是:没有定义的成员,还有因为受到访问控制而访问不到的
<?php  
// PHP 重载
class Credit{
    private $idNum;
    private $age;
    
    public function __construct($idNum,$age){
        $this->idNum = $idNum;
        $this->age = $age;
    }

    // 设置 中转站
    public function __set($name,$value){
    	// 根据属性名称$name 生成对应的属性访问私有接口
        // 'set'.'IdNum' = setIdNum
        $method = 'set'.ucfirst($name);
        // method_exists() 检测对象中是否存在某个方法
        return method_exists($this,$method)?$this->$method($name,$value):null;
    }
    private function setIdNum($name,$value){
    	// property_exists()检测对象中是否存在某个属性
        if(property_exists($this,$name)){
        	return $this->$name = strlen($value)==18?$value:null;
        }
    }
    private function setAge($name,$value){
    	if(property_exists($this,$name) && intval($value)>0){
        	return $this->$name = $value;
        }else{
        	return $this->$name = null;
        }
    }
    
    // 访问 中转站
    public function __get($name){
    	$method = 'get'.ucfirst($name);
         return method_exists($this,$method)?$this->$method($name):null;
    }
    private function getIdNum($name){
    	// 用户修改并查看的属性是否存在,并且属性内容已修改成功,不为空。
        if(property_exists($this,$name) && !empty($this->$name)){
        	return substr_replace($this->$name,"****",-4,4);
        }else{
        	return "身份证信息不合法";
        }
    }
    private function getAge($name){
    	if(property_exists($this,$name) && !empty($this->$name)){
    		if($this->$name<18){
    			return "年龄小于18岁";
    		}elseif($this->$name<35){
    			return "年龄小于35岁";
    		}else{
    			return "年龄大于35岁";
    		}
    	}else{
    			return "年龄不合法";
    	}
    }
}

$c = new Credit('341226186915868525',88);
$c->idNum = "341226186915868585";
echo $c->idNum."<br>";
$c->age = 25;
echo $c->age;
?>

拦截器典型的作用:

1、给出友好提示

2、执行默认操作

方法重载(方法拦截器)

当调用不可访问的方法时,__call会被自动调用, 还会自动传给__call两个参数:分别代表被调用的不存在的方法名 和调用时传递的参数
__callStatic()__call类似,当静态调用一个不可访问的方法时,会自动执行!

<?php  
    // 方法拦截器,当从类的外部,访问类中不存在或无权限访问的方法的时候,会自动调用它
	class  User{
	    public static function normal(){
	        return __METHOD__;
	    }

	    /*
	    *@access public
	    *@param string $method 方法名
	    *@param array $args 调用参数
	     */
    
    	// 方法拦截器 魔术方法 __call(不存在的方法名, 调用时传递的参数数组)
	    public function __call(string $method,array $args){
	    	printf("调用对象方法%s(),参数为:[%s]<br>",$method,implode(",",$args));
	    }
        // 静态方法拦截器: __callSatic(方法名称, 参数数组)
	    public static function __callStatic(string $method,array $args){
	    	printf("调用静态方法%s(),参数为:[%s]<br>",$method,implode(",",$args));
	    }
	}

	(new User)->getInfo('刘亦菲','美女','170cm');
	User::getName("小爱","咩咩",'皮特朱');
	echo User::normal();
?>

事件委托(方法重定向)

访问类中不存在的成员方法时,会被魔术方法拦截,把请求重定向到别的类的成员方法来处理

<?php
	// 被委托的类
    class Base
    {
        public function write(...$args) //...为展开
        {
            printf('方法名: %s(), 参数 [%s]',__METHOD__, implode(', ', $args));
        }
        public static function fetch(...$args)
        {
            printf('方法名: %s(), 参数 [%s]',__METHOD__, implode(', ', $args));
        }
    }


    // 工作类
    class Work
    {
        // 事件委托时,重定向到的目标类
        private $base;
        
        // 将$base初始化
        public function __construct(Base $base)
        {
            $this->base = $base;
        }
        
        // 方法拦截器,将$this->write()重定向到$this->base->write()
        public function __call($name, $args)
        {
            // 将$this->$name()重定向到$this->base->$name()
            if (method_exists($this->base, $name))
            // return $this->base->$name($name, $args); 这个也可以,但基本上不用
            // 用回调的方式来调用: call_user_func_array(函数, 参数数组)
            // 如果是一个对象的方法,应该使用数组的方式: [对象, '方法名']
            return call_user_func_array([$this->base, 'write'], $args);
        }
        
        // 方法拦截器,将self::fetch()重定向到Base::fetch()
        public static function __callStatic($name, $args)
        {
            // 将self::$name()重定向到Base::$name()
            if (method_exists('Base', $name))
            return call_user_func_array(['Base', 'fetch'], $args);
        }
    }

	// 客户端代码
    // $base = new Base();
    // $work = new Work($base);
	$work = new Work(new Base);
    $work->write(11,22,33);
    echo "<br>";
    $work::fetch('皮特朱', '咩咩', '欧阳');

实战案例(查询构造器)

  • 事件委托/方法拦截器 实战: 数据库查询构造器(链式查询)
<?php
// 查询类
class Query
{
    // 连接对象
    protected $db;
    // 数据表
    protected $table;
    // 字段列表
    protected $field;
    // 记录数量
    protected $limit;
    // 添加数据
    protected $values;
    // 操作条件
    protected $where;
    
    // 构造方法: 连接数据库
    public function __construct($dsn, $username, $password) 
    {
        $this->connect($dsn, $username, $password);
    }
    // 连接数据库
    private function connect($dsn, $username, $password)
    {
        $this->db = new PDO($dsn, $username, $password);
    }


    // 设置默认的数据表名称
    public function table($table)
    {
        $this->table = $table;
        return $this;
    }
      // 设置默认的字段名称
    public function field($field)
    {
        $this->field = $field;
        return $this;
    }
    // 链式方法: 设置查询数量
    public function limit($limit)
    {
        $this->limit =empty($limit)?"":"LIMIT $limit";
        return $this;
    }
    // 设置修改或添加的数据
    public function values($values)
    {
        $this->values = $values;
        return $this;
    }
    // 设置where条件
    public function where($where)
    {
        $str = '';
        if (!empty($where)) {
            foreach ($where as $key => $value) {
                if(is_array($where)){
                    if(count($where)>1){
                        $str .= $key.'='.$value.' and ';
                    }else{
                        $str .= $key.'='.$value;
                    }
                }
            }
        }
        $str = count($where)>1?substr($str,0,-4):$str;
        $this->where = empty($where)?"":"where $str";
        return $this;
    }

    // 生成SQL语句
    protected function getSql($key)
    {
        switch ($key) {
            case 'select':
                return sprintf('SELECT %s FROM %s %s %s', $this->field, $this->table, $this->where, $this->limit);
                break;
            case 'update':
                return sprintf('UPDATE %s SET %s = %s %s', $this->table, $this->field, $this->values,$this->where);
                break;
            case 'insert':
                $this->field = $this->field == "*"?"":$this->field;
                return sprintf('INSERT INTO %s (%s) VALUES (%s)', $this->table, $this->field, $this->values);
                break;
            case 'delete':
                return sprintf('DELETE FROM %s %s', $this->table, $this->where);
                break;
        }
    }

    // 执行SQL语句
    public function select()
    {
        return $this->db->query($this->getSql('select'))->fetchAll(PDO::FETCH_ASSOC);
    }
    public function insert()
    {
        return $this->db->query($this->getSql('insert'));
    }
    public function update()
    {
        return $this->db->query($this->getSql('update'));
    }
    public function delete()
    {
        return $this->db->query($this->getSql('delete'));
    }
}



// 数据操作类
class DB
{
    // 静态方法委托
    public static function __callStatic($name, $args)
    {
        // 获取到查询类的对象: new Query()
        $dsn = 'mysql:host=localhost;dbname=myblog';
        $username = 'root';
        $password = 'root';
        $query = new Query($dsn, $username, $password);
        // 直接跳到Query类中的具体方法来调用
        return call_user_func_array([$query, $name], $args);
    }
}

查询记录

$result = DB::table('blogs')->field("*")->select();
echo "<pre>";
print_r($result);

添加记录

$result = DB::table('blogs')->field('*')->values("'2','2','php重定向','事件委托/方法拦截器 实战: 数据库查询构造器(链式查询)', '2021-05-10 15:58:26'")->insert();
 echo "<pre>";
print_r($result);

$result = DB::table('blogs')->field("*")->limit(5)->select();
echo "<pre>";
print_r($result);

修改记录

DB::table('blogs')->field('title')->values('"我的标题被修改了"')->where(["id"=>1])->update();

$result = DB::table('blogs')->field("*")->limit(1)->select();
echo "<pre>";
print_r($result);

删除记录

DB::table('blogs')->where(["user_id"=>"2"])->delete();

$result = DB::table('blogs')->field("*")->select();
echo "<pre>";
print_r($result);

数据库表SQL脚本文件

SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for blogs
-- ----------------------------
DROP TABLE IF EXISTS `blogs`;
CREATE TABLE `blogs` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(11) NOT NULL,
  `title` varchar(128) NOT NULL,
  `content` text,
  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `user_id` (`user_id`),
  CONSTRAINT `blogs_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of blogs
-- ----------------------------
INSERT INTO `blogs` VALUES ('1', '1', '可用的路由方法', '路由器允许你注册能响应任何', '2020-09-26 16:43:00');
INSERT INTO `blogs` VALUES ('3', '1', '基本路由', '构建基本路由只需要一个 URI 与一个 闭包', '2020-09-26 16:42:03');
INSERT INTO `blogs` VALUES ('4', '2', 'web前端开发', 'HTML、css、js', '2020-09-30 22:34:46');
INSERT INTO `blogs` VALUES ('5', '1', '重定向路由', '如果要定义重定向到另一个 URI 的路由', '2020-09-26 17:09:49');

-- ----------------------------
-- Table structure for users
-- ----------------------------
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `account` varchar(20) NOT NULL,
  `password` varchar(32) NOT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

-- ----------------------------
-- Records of users
-- ----------------------------
INSERT INTO `users` VALUES ('1', 'admin', '1234');
INSERT INTO `users` VALUES ('2', 'user1', '0000');

trait: 类功能横向扩展

在PHP中代码只能单继承,为了实现类代码的复用因此实现了trait,trait类与普通class不同它自身无法实例化,只能通过use在class中引用trait,然后这个类就能使用trait中的方法。

共用的一些属性和方法提取出来做公共trait类,就像是装配汽车的配件,如果你的类中要用到这些配件,就直接用use导入就可以了,相当于把trait中的代码复制到当前类中.
因为trait不是类,所以不能有静态成员,类常量,当然也不可能被实例化。

如果说:继承可以纵向扩展一个类,那么trait就是横向扩展一个类功能


通俗点讲:trait就是为了代码复用而生
例一:创建两个trait类,并使用。(trait类不仅可以定义方法,也可以定义属性)

<?php  
trait test1{
	public $name = "zhang";
	public function aa(){
		return "test1";
	}
}

trait test2{
	public function bb(){
		return "test2";
	}
}

class test3{
    //引入关键字 use 
	use test1,test2;
	public function getName($name){
		return "My name is  $name";
	}
}

$obj = new test3;
echo $obj->aa()."<br>";
echo $obj->bb()."<br>";
echo $obj->getName($obj->name)."<br>";
/*
test1
test2
My name is zhang
 */
?>

例二:trait类也可以相互嵌套,一个trait直接使用use引用另一个trait类就行
此处只需要在例一中的test2类中use test1,并去掉test3 类中对test1的引用,代码如下

<?php  
trait test1{
	public $name = "zhang";
	public function aa(){
		return "test1";
	}
}

trait test2{
    use test1;
	public function bb(){
		return "test2";
	}
}

class test3{
	use test2;
	public function getName($name){
		return "My name is  $name";
	}
}

$obj = new test3;
echo $obj->aa()."<br>";
echo $obj->bb()."<br>";
echo $obj->getName($obj->name)."<br>";
/*
test1
test2
My name is zhang
 */
?>

例三、trait类不影响继承父类,但是 如果trait类中存在和父类同名的方法时,返回结果为trait类的方法返回值
(trait类的优先级比父类高)

<?php  
trait test1{
	public $name = "zhang";
	public function aa(){
		return "test1";
	}
}

class test2{
	use test1;
	public function aa(){
		return "test2";
	}
}

class test3 extends test2{
	use test1;
	public function getName($name){
		return "My name is  $name";
	}
}

$obj = new test3;
echo $obj->aa()."<br>";
echo $obj->getName($obj->name)."<br>";
/*
test1
My name is zhang
 */
?>

例四:当本类,trait类和父类都哈有同名方法时,优先使用本类的方法
(优先级 : 本类>trait类>父类)

<?php  
trait test1{
	public $name = "zhang";
	public function aa(){
		return "test1";
	}
}

class test2{
	use test1;
	public function aa(){
		return "test2";
	}
}

class test3 extends test2{
	use test1;
	public function aa(){
		return "test3";
	}
}

$obj = new test3;
echo $obj->aa()."<br>";
// test3
?>

例五:如果两个trait类中含有同名方法(需要为同名方法起别名)

<?php  
trait test1{
	public function aa(){
		return "test1";
	}
}

trait test2{
	use test1;
	public function aa(){
		return "test2";
	}
}

class test3{
	use test1,test2{
		test2::aa insteadof test1;
		test2::aa as bb;
	}
	
}

$obj = new test3;
echo $obj->aa()."<br>";
echo $obj->bb()."<br>";

?>
;