ZendFramework从1.8版开始提供代码生成组件,当时我们开始发布Zend_Tool
。Zend_CodeGenerator
很大程度上模仿了PHP的反射API,但恰恰相反:它生成代码。
为什么要生成代码?
- 您可以将它用作常见任务的“复制和粘贴”辅助形式(例如,它在
zf.sh
中用于生成控制器类和操作方法)。 - 您可能想要从配置生成代码,以移除从配置值生成对象的“编译”阶段。通常这样做是为了在严重依赖可配置值的情况下提高性能。
ZF2存储库中的
Zend\CodeGenerator
主要是从ZendFramework1移植过来的,但也包括一些围绕命名空间使用和导入的功能。我这周在处理一些原型时使用了它,发现它很有用够了,我想分享一些我学到的东西。
基础知识
在大多数情况下,您需要查看API方法以了解您可以创建什么。各种类都在Zend\CodeGenerator\Php
命名空间中(子命名空间是为了让我们在未来的某个时候可以包含PHP以外的格式和语言的代码生成),它们包括:
Docblock\Tag\LicenseTag
(为文档块生成“许可证”注释)Docblock\Tag\ParamTag
(生成“参数””docblocks注释)Docblock\Tag\ReturnTag
(为docblocks生成“return”注释)PhpBody
(生成任意PHP内容;通常用于填充文件或方法调用)PhpClass
(生成PHP类)PhpDocblock
(生成PHPdocblocks)PhpDocblockTag
(生成任意的dockblock注释)PhpFile
(生成PHP文件)PhpMethod
(生成PHP类方法)PhpParameterDefaultValue
(为PHP方法/函数参数生成默认参数值)PhpParameter
(生成PHP方法/函数参数)PhpProperty
(生成PHP类属性)PhpPropertyValue
(生成PHP属性值arg文件;即实例化时的默认属性值)PhpValue
(生成任意PHP值赋值语句)
在大多数情况下,您可以调用setContent()
和/或setName()
方法;其他方法将根据上下文提供。所有类还包含一个generate()
方法,该方法将根据对象的当前状态生成代码。
这些类中的大多数在孤立情况下没有多大用处,而是与其他对象交互以创建预期代码。
例如,我构建的原型正在生成一个PHP类文件。要求包括:
- 设置命名空间
- 定义一个或多个类导入
- 定义一个类,扩展另一个类
- 为该类定义多个方法,带代码;至少在一种情况下,生成的方法还需要参数
这实际上相对容易;最难的部分是为各个方法生成实际的代码体!
例如,我们现在将生成一个类骨架:
use Zend\CodeGenerator\Php as CodeGen; $file = new CodeGen\PhpFile(); $file->setNamespace('Application') ->setUses('Zend\Di\DependencyInjectionContainer', 'DIC'); $class = new CodeGen\PhpClass(); $class->setName('Context') ->setExtendedClass('DIC'); $get = new CodeGen\PhpMethod(); $get->setName('get') ->setParameters(array( new CodeGen\PhpParameter(array('name' => 'name')), new CodeGen\PhpParameter(array( 'name' => 'params', 'defaultValue' => new CodeGen\PhpParameterDefaultValue(array( 'value' => array(), )), )), )); $class->setMethod($get); $file->setClass($class); echo $file->generate();
以上将生成以下内容:
<?php namespace Application; use Zend\Di\DependencyInjectionContainer as DIC; class Context extends DIC { public function get($name, $params = array()) { } }
一些提示和陷阱:
-
与大多数ZF一样,可以配置任何setter方法。键名对应于setter方法,减去“set”,首字母小写—因此,
setName()
可以通过传递配置键“name”来触发;setDefaultValue()
和“defaultValue”。 -
在大多数情况下,您不需要提供对象;您可以传递表示预期对象类型的配置值的数组。例如,将值数组作为项目传递给
setParameter()
会将配置传递给PhpParameter
的构造函数。也就是说,我发现进行显式对象声明更可预测且更易于阅读。 -
如果您的默认参数值是一个数组,则您必须跳过一些环节。通常,您可以简单地为
setDefaultValue()
方法(或“defaultValue”键)指定要使用的值,但数组被视为配置。因此,您需要在这些情况下显式创建一个PhpParameterDefaultValue
(就像我在上面的示例中所做的那样)。 -
在上面,我除了骨架之外没有生成任何东西。然而,在我的实际原型中,我正在为方法的主体内容生成代码。我发现
sprintf
是我的朋友,代表缩进量的变量或常量也是如此。例如:$caseStatements = array(); foreach ($definitions as $definition) { // ... $caseStatement = ''; foreach ($cases as $case) { $caseStatement .= sprintf("%scase '%s':\n", $indent, $case); } $caseStatement .= sprintf("%sreturn \$this->%s();\n", str_repeat($indent, 2), $getter); $caseStatements[] = $caseStatement; } $switch = sprintf("switch (\$name) {\n%s}\n", implode($caseStatements, "\n")); $method->setBody($switch); // PhpMethod object
这又生成了以下内容:
switch ($name) { case 'foo': case 'My\Component\Foo': $this->getMyComponentFoo(); }
为什么?
它可能看起来像很多代码,您可能想知道,“为什么要这么麻烦?”不过,要点在于它是可预测和可测试的——这使它有可能成为模板化解决方案。我基本上可以确保我想要的结构与使用DOM
构造XML相似—如果需要,稍后可以更改它。
此外,在我的特定用例中——实际上,这是一个常见用例——我正在使用可预测的配置结构,并希望一遍又一遍地生成一些东西。当我的配置发生变化时,我希望能够更新代码,而不必担心我忘记了什么或引入了新的错字(除了我在配置文件中创建的错字)。关键是,这是我将一遍又一遍地编写的代码,因此拥有一个生成它的工具将节省我的时间。
此外,在这个特定的用例中,生成的代码比运行生成它的代码更快,因为它避免了最终生产阶段的“配置”步骤。通过生成代码,我可以绕过诸如反射之类的事情,使用更有效的做法(例如,使用call_user_func()
或直接方法调用而不是call_user_func_array()
),并引入类型提示在以前依赖字符串的区域。
完成
Zend\CodeGenerator
中提供了大量的功能,而我在这篇文章中只触及了冰山一角。您有哪些用于代码生成的用例?您有哪些技巧可以分享?