Java有注解和反射,Ruby有代码生成代码的元编程,Scala有Monad函子,而PHP有魔术方法。这些都是非常强大的武器,有人喜欢它的强大,但也有人讨厌它的复杂以及伴随而来的难以理解、万丈深渊。例如Ruby中的猴子补丁,非线性顺序的执行经常会让人摸不着头脑。另一方面,如果能够深入理解PHP的魔法方法,并加以灵活、恰当地使用,你将能节省很多重复性的代码编写,具备在陌生环境更好的代码生存能力,还能对某些看似神奇的现象做出合理的解释。
下面,我们将来一起探索这块魔法之地。
继续探讨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 __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操作,则抛出异常,告知开发人员非法调用。
魔法方法与代码生成
顺便说一下,魔法方法都是以双下划线开头的。此外,引申两点。先说简短的,再说稍长的。第一点, 当调用对象中一个不存在的方法时,会触发__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()
这个方法即可,非常简单。
<?php class DTO { public function __call($method, $params) { if (substr($method, 0, 3) == 'set') { $key = lcfirst(substr($method, 3)); $this->$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,并在内放置以下实现代码:
<?php // DTO简易代码生成器 $class = $argv[1]; $properties = array_slice($argv, 2); $code = "<?php class $class { "; foreach ($properties as $it) { $itUpper = ucfirst($it); $code .= " public function set{$itUpper}(\${$it}) { \$this->{$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代码:
<?php class Student { public function setName($name) { $this->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.php namespace 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命令,可自动生成测试代码。
如果想提升自己的开发效率,提升整个项目的交付速度,魔术方法或者代码生成,都是值得推荐的策略。前者可以节省编写重复的代码,后者则可以直接帮你生成重复的代码。