在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状态。
