PHP高级编程-回归原生态-数组类和魔术方法



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()这个方法即可,非常简单。

<?phpclass 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 = "<?phpclass $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代码:

<?phpclass 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.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命令,可自动生成测试代码。


如果想提升自己的开发效率,提升整个项目的交付速度,魔术方法或者代码生成,都是值得推荐的策略。前者可以节省编写重复的代码,后者则可以直接帮你生成重复的代码。何乐而不为?


本文分享自微信公众号 - 小白开放平台(yesapi)。
如有侵权,请联系 [email protected] 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章