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() 打印对象信息时自动调用 |
注意: 这些魔术方法的参数都不能通过引用传递。
属性重载(属性拦截器)
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'; // __setecho $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>";/*test1test2My 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>";/*test1test2My 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>";/*test1My 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>";?>