在ZendFramework2.0中,我们在许多方面进行了重构,以提高框架的一致性。我们早期发现的一个领域是如何加载插件。
ZendFramework中的“插件”一词适用于许多项目:
- Helpers(视图助手、动作助手)
- 应用程序资源
- 过滤器和验证器(特别是应用于
Zend_Filter_Input
和时Zend_Form
) - 适配器
几乎在每种情况下,我们都使用“短名称”来命名插件,以便允许动态加载它。这允许更简洁的代码,以及配置代码以允许指定替代实现的能力。
分析
在1.0.0之前,我们创建了“PluginLoader”,一个用于将插件名称解析为其完整类名称的类。虽然这个解决方案工作得相当好,但它绝不是完美的——事实上远非如此:
- 它只处理类解析,而不处理实际的类实例化或持久化,这导致:
- 使用它的每个组件通常以不同方式处理类实例化和注册。
- 一些组件只是决定不使用该解决方案,要么是因为它不够全面,要么是因为他们需要处理边缘情况;这会导致:
- 区分大小写的问题。如果插件名称不遵循原始类大小写,可能会出现各种问题;在区分大小写的文件系统上,将找不到插件,在不区分大小写的文件系统上,将找到插件文件,但找不到类——导致错误不一致。组件处理插件区分大小写的方式也导致API不一致。
- 堆栈解析问题。插件以“前缀路径”对的形式加载到堆栈中……每个前缀都可能存储一堆要查找的路径。了解将解析哪个前缀和路径可能很困难——尤其是在可能自动添加路径的MVC中。这也会导致一个关键问题:
- 性能问题。前缀/路径解决方案需要系统统计调用。事实上,在许多情况下,相同的插件会在单个请求的过程中被加载多次,但由于不同的对象负责,相同的查找和统计调用将被多次调用。统计调用很昂贵;事实上,我们发现插件加载可能是整个框架中成本最高的操作!
一些问题示例:
Zend_Application
中的资源应该不区分大小写。这导致出现奇怪的类名,例如“Frontcontroller”、“Cachemanager”等。- 许多开发人员将“doctype”视图帮助程序名称(“docType”)驼峰式命名——导致错误。
- >由于默认模块允许使用applicationprefix或
Zend_View_Helper
前缀进行注册,因此对于将加载哪个helper经常存在冲突。
这些问题的最终结果是ZendFramework中的插件方法不一致,导致严重的性能下降。
介绍PluginBroker
在分析情况时,我们确定以下职责应该并且可以跨组件共享:
- 插件类解析
- 插件类实例化
- 插件注册
基本上,我们看到了许多设计模式,包括LazyLoading、Factory、Builder和Registry。我们将它们分成Zend\Loader
命名空间中的多个接口:
- ShortNameLocater
- 代理
- LazyLoadingBroker
第一个接口,ShortNameLocater
,描述了将插件名称解析为类的行为。代码通常会简单地使用接口,它由非常简单的方法组成,用于从插件名称加载(解析)类,并检查给定的插件名称是否已被解析。
第二个Broker
描述了一个类,该类执行以下操作:
- 组成一个
ShortNameLocater
- 实例化和注册插件
最后一个是LazyLoadingBroker
,它扩展了Broker
并添加了预先指定实例化选项以及要加载的插件列表的功能。这方面的用例包括Zend\Application
,您可能希望在其中配置要加载的资源列表,以及可选的实例化选项。
插件类解析
我们包括ShortNameLocater的两个实现。第一个替换了原来的PluginLoader
,称为PrefixPathLoader
。在内部,它已被重构以利用SplStack
和SplFileInfo
,这两者都具有更高的性能并且可以更好地跨平台工作。
第二种实现是ZF2中现在使用的标准,称为PluginClassLoader
。它实现了一个非常简单的插件/类哈希机制,允许我们利用自动加载器进行查找并快速返回结果。它还简化了围绕覆盖插件的故事:您只需为给定的插件名称注册一个不同的类,这使得它非常容易在您的代码中搜索此类情况。
一个简单的PluginClassLoader
扩展可能如下所示:
namespace Zend\Paginator; use Zend\Loader\PluginClassLoader; class AdapterLoader extends PluginClassLoader { /** * @var array Pre-aliased adapters */ protected $plugins = array( 'array' => 'Zend\Paginator\Adapter\ArrayAdapter', 'db_select' => 'Zend\Paginator\Adapter\DbSelect', 'db_table_select' => 'Zend\Paginator\Adapter\DbTableSelect', 'iterator' => 'Zend\Paginator\Adapter\Iterator', 'null' => 'Zend\Paginator\Adapter\Null', ); }
这种方法使得在每个组件的基础上提供预期插件的预设变得简单。要重载定义(或创建新定义),请注册:
$loader->registerPlugin('array', 'Foo\Paginator\CustomArrayAdapter');
因为您可能希望在您的应用程序中全局覆盖某些插件名称,我们还通过addStaticMap()
方法提供一些静态访问。
Zend\Paginator\AdapterLoader::addStaticMap(array( 'array' => 'Foo\Paginator\CustomArrayAdapter', ));
优先级如下:
- 显式注册的地图(
registerPlugin()
,传递给构造函数的地图)总是获胜,其次是 - 静态注册的地图(
addStaticMap()),然后是
- 类中定义的映射
注册插件,无论是静态完成还是按实例完成,都会覆盖该实例的映射条目——这意味着查找速度很快。
插件实例化和注册
插件类解析后的下一个难题是如何实例化和注册插件类。如分析中所述,在ZF1中,这是以每个组件的临时方式完成的。Broker
接口使流程标准化。该接口定义了以下内容:
namespace Zend\Loader; interface Broker { public function load($plugin, array $options = null); public function getPlugins(); public function isLoaded($name); public function register($name, $plugin); public function unregister($name); public function setClassLoader(ShortNameLocater $loader); public function getClassLoader(); }
获得以下好处:
- 您可以指定要传递给构造函数的参数。
- 您可以注册插件的显式实例,以及动态加载它们。
- 如果插件具有之前已由当前代理实例加载(或明确注册),它将立即返回。
- 您可以获得所有已加载插件的列表(对于确定应用程序依赖性很有用)。
- 您可以指定要使用的插件类解析器。
LazyLoadingBroker
实现扩展了Broker
,并添加了以下方法:
namespace Zend\Loader; interface LazyLoadingBroker { public function registerSpec($name, array $spec = null); public function registerSpecs($specs); public function unregisterSpec($name); public function getRegisteredPlugins(); public function hasPlugin($name); }
LazyLoadingBroker
背后的想法是,您可能想要指定在加载特定插件时应使用哪些选项,但不想立即加载它(或者可能根本不加载它)。此外,您可能希望获得以这种方式注册的插件列表——例如,迭代它们以便对每个插件进行操作。典型的例子是应用程序资源、表单过滤器、验证器和装饰器。
现在,我将重点关注PluginBroker
类,它是Broker
接口的通用实现。它旨在满足大多数使用某种插件的组件的需求。默认情况下,它将延迟加载一个空的PluginClassLoader
,但允许您指定默认值。此外,它还提供了一个用于验证已注册插件的挂钩,以确保您加载插件的组件内的一致性。
后者是确保代理返回的对象类型一致的关键。在最基本的情况下,您可以通过setValidator()
方法将任何有效的回调注册为验证器;最简单的方法是使用闭包:
$broker->setValidator(function($plugin) { if (!$plugin instanceof Plugin) { throw \RuntimeException('Invalid plugin'); } return true; });
然而,在内部,register()
方法调用受保护的validatePlugin()
方法,该方法将调用已注册的验证器回调(如果有)。这提供了一个很好的扩展点,我们在框架中使用它。
例如,上面的Zend\Paginator\AdapterLoader
类的对应类如下:
namespace Zend\Paginator; use Zend\Loader\PluginBroker; class AdapterBroker extends PluginBroker { /** * @var string Default plugin loading strategy */ protected $defaultClassLoader = 'Zend\Paginator\AdapterLoader'; /** * Determine if we have a valid adapter * * @param mixed $plugin * @return true * @throws Exception */ protected function validatePlugin($plugin) { if (!$plugin instanceof Adapter) { throw new Exception\RuntimeException('Pagination adapters must implement Zend\Paginator\Adapter'); } return true; } }
此代理使用AdapterLoader
作为其默认类加载器,并挂接到validatePlugin()
以测试插件实例是否为Adapter
实例;否则,它会引发异常。
在使用插件的类中,您可以设置访问器和修改器来检索和设置PluginBroker实例,然后简单地使用代理。例如,Paginator
中的以下行加载并注册适当的适配器:
// Assume $adapter is an adapter name, and $data is an array or object to pass // to the constructor $broker = self::getAdapterBroker(); $adapter = $broker->load($adapter, array($data)); return new self($adapter);
这减少了该特定组件中的大量代码——实现从几十行减少到不到十几行,而且更加灵活。
使用这种方法有利也有弊。在专业方面,我们减少了代码量,同时提供了更灵活、可注入的解决方案。不利的一面是,您通常会在Broker
接口上进行暗示——这意味着可能会使用不符合预期适配器的插件。然而,我们认为这是一个边缘案例,并且认为如果您这样做,您可能知道所涉及的问题。
PluginSpecBroker
大多数情况下使用PluginBroker
。但是,在许多情况下存在以下工作流程:
- 对象定义了它将在未来某个时刻使用的插件的规范。
- 此时,它循环遍历这些规范,延迟加载类并使用它们。
- 李>
同样,示例是Zend\Application
资源,以及(currentincarnation)表单元素、装饰器、验证器和过滤器。另一个例子是Zend\Filter\InputFilter
,它通常在使用前配置好。
为了这些目的,我们定义了接口LazyLoadingBroker
,我在前面详细介绍过。它的具体实现是PluginSpecBroker
,它扩展了PluginBroker
并实现了LazyLoadingBroker
。这几乎完全像PluginBroker
一样使用,在工作流程中有一些细微差别。
如前所述,您通常会预先配置此代理,以便您可以通过配置文件尽早定义它。
例如,您可能具有以下配置:
resources.frontcontroller.module_directory = APPLICATION_PATH "/modules" resources.view.encoding = "iso-8859-1" resources.view.doctype = "html5" resources.layout.layout_path = APPLICATION_PATH "/layouts/scripts/" resources.layout.layout = "layout"
配置可能会按如下方式传递:
// in the Zend\Application namespace: $broker = new ResourcesBroker($config->resources);
然后,稍后,您的代码会循环访问这些插件,检索它们,并对它们进行操作:
foreach ($broker->getRegisteredPlugins() as $resource) { // do something with $resource... }
在我们的例子中,我们将遍历“frontcontroller”、“view”和“layout”资源,并为每个资源提供适当的配置。
如果要循环多次,您会立即受益:插件已经存在并已实例化!
状态
我们在过去几周内完成了ZF2的“自动加载和插件加载”里程碑。这涉及重构所有使用旧PluginLoader解决方案的地方,以改用新PluginBroker。
然而,有一些异常值:
Zend\Cache
目前正在重构,并将在这项工作期间或完成时合并更改。Zend\Form
仍然需要更新。但是,我们正在考虑使用ValidatorChain
和FilterChain
对象(这可能意味着修改它们以实现LazyLoadingBroker
),我们也可能会更改形式和元素将会出现——这可能意味着消除插件代理需求。因此,可能需要就位的唯一中介是元素本身。
Zend\View
被重构为使用PluginBroker
和FilterChain
。事实上,Zend\View
中重构了大量功能,并且在MVC里程碑期间还会出现更多功能。
概要
在结束ZF2的自动加载/插件加载里程碑时,我们已经完成了提高框架一致性的重要目标,同时还提高了框架的性能。早期的基准测试表明,如本文所述,将新的自动加载系统与插件代理系统结合使用,我们可以将性能提高7到20倍。让它沉入片刻。基本功能保持不变,只是对插件的检索方式进行了一些微小的API更改——但通过这些更改,我们可以在框架性能方面取得重大改进。就我而言,这是一个双赢的局面。
您可以通过关注我们的GitHub存储库或下载2.0.0dev2快照来查看ZF2状态。