4.3 活用魔术方法
Java有注解和反射,Ruby有代码生成代码的元编程,Scala有Monad函子,而PHP有魔术方法。这些都是非常强大的武器,有人喜欢它的强大,但也有人讨厌它的复杂以及伴随而来的难以理解、万丈深渊。例如Ruby中的猴子补丁,非线性顺序的执行经常会让人摸不着头脑。
另一方面,如果能够深入理解PHP的魔法方法,并加以灵活、恰当地使用,你将能节省很多重复性的代码编写,具备在陌生环境更顽强的代码生存能力,还能对某些看似神奇的现象做出合理的解释。
下面,我们将来一起踏上这片魔法之地。
4.3.1 继续探讨DI容器背后的技巧
前面有说到Phalcon和PhalApi这两个PHP开源框架的DI容器,也见识了它的数组访问形式。但它的使用方式不止这一种,还有两种是和本次要讨论的魔法方法有关。我们先来看最终客户端的使用效果,再反过来追寻它背后的实现和原理。
以PhalApi框架为例,对于服务资源的注册和获取,还可以通过类属性以及类成员函数来操作。例如:
// 通过类属性方式操作
$di->request = new \PhalApi\Request();
var_dump($di->request);// 通过类成员函数方式操作
$di->setRequest(new \PhalApi\Request());
var_dump($di->getRequest());
这样是不是很酷?!开发工程师完全可以根据自己的喜爱来选择操作方式,不用再担心会忘记如何使用DI容器。那么这些炫酷的特效是如何实现的呢?
如果查看PhalApi框架中DependenceInjection类的源代码,是找不到上面这些类属性和类成员函数的。事实上,它也不可能穷举全部开发人员会用到哪些资源服务。为此,只能使用动态的方式来维护。如果细心品读DependenceInjection类的源代码,我们可以找到魔法方法的影子,顺着这些蛛丝马迹,我们就能领略魔法方法的美妙之处。
在给不可访问属性赋值时,__set() 会被调用。读取不可访问属性的值时,__get() 会被调用。所以,当对$di->request进行赋值时,会触发DependenceInjection内的__set()方法,对应代码是:
public function __set($name, $value) {
$this->set($name, $value);
}
而当通过$di->request获取不存在的属性时,会触发DependenceInjection内的__get() 方法,对应代码是:
public function __get($name) {
return $this->get($name, NULL);
}
通常情况,__set() 和__get() 是配套使用的。
再来看下另外一个魔法方法——__call(),当在对象中调用一个不可访问的方法时,就会触发这个魔法方法。例如,执行$di->setRequest()操作时,就会触发DependenceInjection内的__call()方法,对应代码是:
public function __call($name, $arguments) {
if (substr($name, 0, 3) == 'set') {
$key = lcfirst(substr($name, 3));
return $this->set($key, isset($arguments[0]) ? $arguments[0] : NULL);
} else if (substr($name, 0, 3) == 'get') {
$key = lcfirst(substr($name, 3));
return $this->get($key, isset($arguments[0]) ? $arguments[0] : NULL);
}
throw new InternalServerErrorException(
T('Call to undefined method DependenceInjection::{name}() .', array('name' => $name))
);
}
稍微解释一下,__call()方法的第一个参数是要调用的方法名称,第二个参数是数组类型,即传递过来的参数列表。在这里,先判断调用的方法是以set还是以get开头,然后如果有传递参数再将参数列表传递下去。最后如果既不是set也不get操作,则抛出异常,告知开发人员存在非法调用。
4.3.2 魔法方法与代码生成
顺便说一下,魔法方法都是以双下划线开头的。此外,引申两点。先说简短的,再说稍长的。第一点, 当调用对象中一个不存在的方法时,会触发__call()魔法方法,那如果尝试调用的是类的静态方法,又会触发哪个魔术方法呢?答案是:__callStatic()。它的参数以及功能,和__call()类似,唯一不同点是名称以及需要使用static关键字,它的函数签名是:
public static mixed __callStatic ( string $name , array $arguments )
有兴趣的同学可以自行实现一个具体的示例,并尝试对它进行使用。
第二点是,有人担心过多调用魔法方法会影响性能,因此会禁用魔法方法。但我觉得,既然选择了PHP这门语言,就不会过多关注相差几毫秒的性能。事实上,大型系统的性能瓶颈都不在于语言的执行层面,而主要集中于I/O方面,例如文件I/O,网络I/O,数据库I/O。但这也给了我们另一个启发,如果确实需要关注性能,我们也可以对于常见的setter/getter提前生成相应的PHP代码。例如针对数据传输对象DTO,就可以使用这一招。
先来看下,使用魔术方法的实现方式。很简单,起一个合适的类名,然后重载__call()这个方法即可,非常简单。
$key = $params[0];
} else if (substr($method, 0, 3) == 'get') {
$key = lcfirst(substr($method, 3));
return isset($this->$key) ? $this->$key : NULL;
}
}}
出于简单性,这里暂时不对异常的情况作过多的预防和处理。同样,客户端使用setter/getter也是非常简单的。例如这样:
$dto = new DTO();
$dto->setName('dogstar');
var_dump($dto->getName());
这些都是没什么难度的,一旦你熟悉魔法方法后。如果在大型企业系统中,想获得更多细致的控制权,也可以为此提前自动生成setter/getter的代码。编写一个代码生成器,对于初学者来说会有点难度,甚至对于从没接触过这块的同学来说也会有点陌生。但一旦在实际项目中应用过后,你就会发现其实代码自动生成也是很简单的,而且应用场景很多。这里以自动生成setter/getter代码为例,先简单说一下实现的思路,再来介绍代码生成在各大开源框架中的应用场景。
每个DTO的类代码,类名是不一样的,另外各自的类属性也是不尽相同的。如果我们能手动编写其中一个DTO的类代码,就能知道其它DTO的类代码要如何生成了。快速来写一个代码生成器脚本 ,命名为:generate_dto_class.php,并在内放置以下实现代码:
{$it} = \${$it};
}
public function get{$itUpper}() {
return \$this->{$it};
}
";}
$code .= "}";
file_put_contents(dirname(__FILE__) . '/' . $class . '.php', $code);
echo "OK!\n";
开发完成后,执行以下命令:
$ php ./generate_dto_class.php Student name age
就可以生成一个Student的DTO类,里面有两个类成员属性,分别是name和age。并且,可以看到在生成的Student.php文件里有以下自动生成的PHP代码:
name = $name;
}
public function getName() {
return $this->name;
}
public function setAge($age) {
$this->age = $age;
}
public function getAge() {
return $this->age;
}
}
是不是很有趣?
在代码生成这一领域,不同的开源框架有不同的做法。Yii框架提供了Gii,一个强大的基于Web 的代码生成器,可以生成Model类的代码,以及CRUD代码。Symfony框架则可以使用Doctrine组件提供的命令来创建Entity实体类的代码。例如输入以下命令并按提示操作:
$ php bin/console make:entity
Class name of the entity to create or update:> Product
最后可以生成类似这样的代码:
// src/Entity/Product.phpnamespace App\Entity;use Doctrine\ORM\Mapping as ORM;/**
* @ORM\Entity(repositoryClass="App\Repository\ProductRepository")
*/class Product{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
private $id;
public function getId()
{
return $this->id;
}
// ... getter and setter methods
}
代码自动生成更多是应用在与数据库操作相关的层级上,例如DTO、实体Entity、模型Model。在我曾经任职的第一家公司里,也提供了一个强大的命令,可以根据xml的配置,自动生成相应的整套数据库相关操作的代码库。另一方面,在其他场景也可以发现代码生成的身影。例如,在PhalApi框架中,提供了phalapi-buildtest命令,可自动生成测试代码。
如果想提升自己的开发效率,提升整个项目的交付速度,魔术方法或者代码生成,都是值得推荐的策略。前者可以节省编写重复的代码,后者则可以直接帮你生成重复的代码。何乐而不为?
相关文章
本站已关闭游客评论,请登录或者注册后再评论吧~