ZF2的MVC模块主要采用了Service Locator与事件驱动的设计模式。下面先简单介绍下这2部分内容。
1、Service Locator
在ZF2中,新增了一个Zend\ServiceManager组件,该组件实现了Service Locator模式。在ZF2中,任何的类库,扩展等资源都统一被视为服务对象,并在ServiceManager组件中注册。当应用程序需要某个功能,例如Zend\Log时,推荐的做法是在ServiceManager中获取,而不是像传统方式那样,include类文件之后再new一个对象出来。这种方式的好处在于解除了服务调用者与服务提供者之间的耦合,作为一名用户,不需要知道Zend\Log位置是在哪里,是如何产生的,只需要知道用Zend\ServiceManager::get()这种方式来取得它就行了。
要在ServiceManager中注册服务对象,有下面几种方法:
直接对象:调用setService方法,可以直接把某个对象注册为一个服务对象。
01
02
03
|
$serviceManager
->setService(
'Log'
,
new
Zend\Log\Logger());
$logger
=
$serviceManager
->get(
'Log'
);
$serviceManager
->setService(
'settings'
,
array
(
'password'
=>
'123456'
));
|
延迟对象:调用setInvokableClass方法,其实就是先注册一个类的名字,当需要使用该服务对象时,ServiceManager实例化一个对象给你。
01
02
|
$serviceManager
->setInvokableClass(
'Log'
,
'Zend\Log\Logger'
);
$logger
=
$serviceManager
->get(
'Log'
);
|
工厂对象:调用setFactory方法,有许多服务对象的产生比较复杂,比如根据配置文件的内容来产生对象等等,这时就需要有个专门的“工厂”来产生服务对象。工厂可以是匿名函数,也可以是实现了 Zend\ServiceManager\FactoryInterface的类或对象。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
use
Zend\ServiceManager\FactoryInterface;
use
Zend\ServiceManager\ServiceLocatorInterface;
class
LogFactory
implements
FactoryInterface
{
public
function
createService(ServiceLocatorInterface
$serviceLocator
)
{
return
new
Zend\Log\Logger();
}
}
// 实现了Zend\ServiceManager\FactoryInterface
$serviceManager
->setFactory(
'Log1'
,
new
LogFactory());
$serviceManager
->setFactory(
'Log2'
,
'LogFactory'
);
$logger
=
$serviceManager
->get(
'Log1'
);
// 匿名函数
$serviceManager
->setFactory(
'service3'
,
function
() {
return
new
\stdClass(); });
|
抽象工厂对象:如果要取得一个名字不存在的服务对象时,ServiceManager会去寻找注册的抽象工厂,抽象工厂实现了Zend\ServiceManager\AbstractFactoryInterface,如果有某个抽象工厂的canCreateServiceWithName方法返回真,则ServiceManager返回该抽象工厂的createServiceWithName方法所产生的对象。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
use
Zend\ServiceManager\ServiceLocatorInterface;
use
Zend\ServiceManager\AbstractFactoryInterface;
class
MyAbstractFactory
implements
AbstractFactoryInterface
{
public
function
canCreateServiceWithName(ServiceLocatorInterface
$serviceLocator
,
$name
,
$requestedName
)
{
// this abstract factory only knows about 'foo' and 'bar'
return
$requestedName
===
'foo'
||
$requestedName
===
'bar'
;
}
public
function
createServiceWithName(ServiceLocatorInterface
$serviceLocator
,
$name
,
$requestedName
)
{
$service
=
new
\stdClass();
$service
->name =
$requestedName
;
return
$service
;
}
}
$serviceManager
->addAbstractFactory(
'MyAbstractFactory'
);
var_dump(
$serviceManager
->get(
'foo'
)->name);
// foo
var_dump(
$serviceManager
->get(
'bar'
)->name);
// bar
var_dump(
$serviceManager
->get(
'baz'
)->name);
// exception! Zend\ServiceManager\Exception\ServiceNotFoundException
|
服务对象的初始化:ServiceManager还提供了对服务对象的额外初始化功能,调用addInitializer()方法可以注册对象初始化器,每当调用ServiceManager的get()方法返回服务对象前,都会对该对象调用所有注册的初始化器,该功能一般用于对第三方开发的服务对象的初始化。
看到这里,如果有熟悉Java的,特别是Spring框架的同学,可能会发现这ServiceManager与Spring中的Ioc容器有点相似。没错,其实两者都是为了实现一个目的:把对象的生产者与对象的使用者之间的耦合性降到最低。Spring中采用的是依赖注入的模式,而ZF2中原本也打算使用用依赖注入的模式,还专门开发了Zend\Di组件,却在最后一个测试版中换成了ServiceLocator模式。但无论采用哪种模式,最终的目的与产生的效果还是一样的:即降低耦合性。我们在使用ZF2的时候也要注意这一点:对象的产生不应该是由对象的使用者决定的,使用者只是选择采用哪一个对象而已。
2、事件驱动
在ZF2中,大量使用了事件驱动的模式。在ZF2中,提供了一个新的Zend\EventManager组件来实现事件驱动。打开Zend\EventMan ager目录,可以看到很多都是一些接口(Interface),核心的就是两个文件,Event.php和EventManager.php,望文生义,一个是事件类(Event),一个是事件管理类(EventManager)。
先说说EventManager类,EventManager类主要负责绑定(attach)事件、解除(detach)事件、触发事件(trigger),在EventManager类内部,维护着一个事件数组,事件数组维护者事件名称与事件处理函数的一对多的对应关系,也就是说一个事件名称可以绑定多个事件处理对象(函数),当事件被触发后,按照绑定顺序,依次执行所绑定的事件处理对象(函数),事件处理对象(函数)也被称为监听器(Listener)。
举个例子可能来理解起来比较清楚:有个论坛用户发文章的功能,每当保存数据到数据库之前,会触发一个名为preSaveContent的事件,我们可以为该事件绑定多个处理函数,比如检查用户权限的函数,过滤敏感字符的函数,判断IP的函数等等,这些函数会以绑定顺序依次执行。
再说说Event类,Event类实例化后就是Event对象,该对象保存了事件的名称,事件发生时的上下文,需要传递给事件处理函数的一些参数。值得一提的是stopPropagation方法,该方法阻止了事件的继续处理,也就是说执行了该方法后,事件队列中后面的处理函数将不会执行。
在ZF2中预定义了一些不同的事件对象,如ModuleEvent、MvcEvent、ViewEvent、SendResponseEvent等等,他们都继承于Zend\EventManager\Event。这些不同的事件对象所起的作用其实是一样的,都是在事件驱动的工作流程中把事件发生时的上下文信息传递给监听器(Listener),不同之处在于事件发生时的逻辑环境。任何程序在逻辑上总是会有不同的执行阶段的,在不同阶段的上下文环境是不同的,如果用一个事件对象来贯穿所有的执行阶段,必然会在该对象上附加所有阶段的上下文信息,从而导致该对象的臃肿,程序结构也不清晰。因此好的设计方法就是在不同的逻辑环境中,使用不同的事件对象。
下面介绍一下ZF2中MVC的启动过程,在ZF2的官网中,提供了一个例子程序: http://framework.zend.com/manual/2.2/en/user-guide/skeleton-application.html ,这里就用这个例子作为说明对象。
把官网的示例程序导入Zend Studio后,是下图的结构:
其中,vendor目录一般存放第三方开发的或是自己开发的通用的模块(Module),而module目录一般存放当前项目的模块(Module)。在ZF2中,每个Module就是一个完全独立的功能模块,可以拥有自己的配置文件、语言文件等等。
与ZF1一样,入口文件是public目录下的index.php文件,该文件一共就3行代码:
01
02
03
04
05
06
07
08
09
10
11
12
|
<?php
/**
* This makes our life easier when dealing with paths. Everything is relative
* to the application root now.
*/
chdir
(dirname(__DIR__));
// Setup autoloading
require
'init_autoloader.php'
;
// Run the application!
Zend\Mvc\Application::init(
require
'config/application.config.php'
)->run();
|
前两行是设置一个PHP类的自动加载器(autoloading),第三行才是真正的MVC的启动代码。
ZF2在逻辑上把一个PHP项目看作一个服务端程序,而不是分散的页面,所以ZF2在设计MVC框架的时候,在逻辑上把运行过程分为两个步骤:1、各模块(Module)以及服务对象的初始化;2、程序运行。
1、初始化
初始化主要功能就是读取配置文件,根据配置文件中的内容生成一个个服务对象并进行适当的初始化设置、注册各类事件并绑定事件处理对象(函数)。这里有一点要提一下,官方推荐的实例布局中,可以看到配置文件分散在许多地方,其中每个模块(Module)下面都有一个config目录,用来存放模块自己的配置文件,其内容称为模块配置信息(ModuleConfig);而在项目根目录下,有一个config目录,存放的是与整个项目结构(诸如有哪些模块、模块存放在哪里等等)有关的配置文件,其称为应用程序配置信息(ApplicationConfig);而autoload目录下存放的是与项目版本,环境有关的配置文件,他们会覆盖module目录下配置文件的内容,其内容称为全局配置信息(GlobConfig),其中goloal.php一般存放与发布的版本有关的配置信息,例如版本号等,而local.php一般存放与环境有关,涉及安全方面的一些配置信息,如数据库的密码等。
初始化执行的是Zend\Mvc\Application的init()方法,一共五行代码,完成的工作可不少,我们一行行看。第一、二行读取配置文件中service_manager和listerners的内容,这里先略过,因为示例代码也没有用到这两部分配置,所以我们这里也不去关注它们,等对ZF2熟悉了以后,自然就能搞明白了。
01
02
|
$serviceManager
=
new
ServiceManager(
new
Service\ServiceManagerConfig(
$smConfig
));
$serviceManager
->setService(
'ApplicationConfig'
,
$configuration
);
|
如上图,接下来是产生一个ServiceManager对象,上面说过了,ZF2用到了Service Locator模式,程序中用到的服务,对象甚至全局参数都可以从ServiceManager中获取,这里生成的ServiceManager对象可以称它为全局ServiceManager对象(或者称之为ServiceLocator对象)。为了方便在程序中获取ServiceLocator对象,ZF2提供了一个ServiceLocatorAwareInterface接口,凡是实现了ServiceLocatorAwareInterface接口的类,都能通过调用getServiceLocator()方法来取得ServiceLocator对象。
ServiceManager可以有许多个,一般按照存放的内容不同来分类,例如专门存放数据库对象的ServiceManager,或专门存储日志对象的ServiceManager,而ServiceManager对象本身又可以保存在全局的ServiceManager对象中。
ZF2中就是这么做的,一个全局的ServiceManager对象中还保存其他的如ControllerPluginManager、ViewHelperManager、ValidatorManager之类的ServiceManager对象,就拿ControllerPluginManager来说,它保存的是各种控制器插件,ZF2提供了许多的控制器插件,如果把这些控制器插件对象都保存在全局的ServiceManager对象中,会显得有点臃肿杂乱,所以在全局的ServiceManager对象中就保存了一个ControllerPluginManager对象,而在ControllerPluginManager对象中保存那些五花八门的控制器插件,获取的时候,就可以这么获取:
01
|
$serviceManager
->get(
'ControllerPluginManager'
)->get(
'redirect'
);
|
ServiceManager对象初始化的时候,默认注册了两个工厂,分别是事件管理器工厂(EventManagerFactory)和模块管理器工厂(ModuleManageFactory),是在Zend\Mvc\Service\ServiceManagerConfig中注册的,后面我们会看到在另一个类中注册了许多其他的服务对象。事件管理器工厂比较简单,仅仅创建一个事件管理器(EventManager)对象,该对象负责整个MVC流程中所有事件的绑定以及触发。而模块管理器工厂比较复杂,在创建模块管理器(ModuleManager)对象时,还绑定了模块初始化过程中的一些默认事件监听对象(Listener)。
在程序初始化阶段,有一个重要的部分就是各模块(Module)的初始化,模块(Module)初始化过程中,有四个事件(模块初始化事件)会被触发,依次是:loadModules(开始载入所有模块),loadModule.resolve(单个模块开始解析),loadModule(单个模块开始载入),loadModules.post(所有模块载入完成),以上这些事件定义在Zend\ModuleManager\ModuleEvent类中。在ZF2中,为这些事件绑定了许多默认的监听器(Listener),大多数监听器的绑定由Zend\ModuleManager\Listener\DefaultListenerAggregate类完成,从名字就可看出,它是一个默认监听器的聚合类。
另外还绑定了一个Zend\ModuleManager\Listener\ServiceListener监听器,该监听器是独立绑定的,并不包括在DefaultListenerAggregate内。ServiceListener是个比较复杂的监听器,它主要合并各模块(Module)的配置信息,并根据配置信息注册服务对象。ServiceListener监听器是由ServiceListenerFactory产生的,ServiceListenerFactory在产生ServiceListener监听器后,还为它设置了许多默认的与MVC有关的服务对象。那么为什么这些服务对象不在Zend\Mvc\Service\ServiceManagerConfig类中注册呢?因为这些对象中用到的许多参数是可配置的,由ServiceListener把配置信息中的相关数据传递给它们。
模块管理器(ModuleManager)负责管理所有的模块(Module),具体有哪些模块需要处理,则是在配置文件中写明的,在application.config.php文件返回的配置信息数组中,有一项名为modules的配置选项就是告诉模块管理器有哪些模块需要被处理。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
return
array
(
'modules'
=>
array
(
'Application'
,
'Album'
,
'Blog'
,
),
'module_listener_options'
=>
array
(
'config_glob_paths'
=>
array
(
'config/autoload/{,*.}{global,local}.php'
,
'module_paths'
=>
array
(
'./module'
,
'./vendor'
,
),
),
)
|
每个模块下都必须有个Module类,该类用来配置当前模块。
当ServiceManager对象产生并初始化之后,接下去就该是各模块(Module)的初始化了:
01
|
$serviceManager
->get(
'ModuleManager'
)->loadModules();
|
模块(Module)的初始化,是由ModuleManager触发相关事件(上文提到的模块初始化事件)后完成的。这是ZF2中事件驱动机制的第一次运作,后面将会贯穿程序运行的整个生命周期。
下面看看模块(Module)的初始化究竟做了哪些事情,首先从ServiceManager中取出模块管理器(ModuleManager),然后执行loadModules()方法,该方法很简单,就是触发loadModules与loadModules.post事件。上面说过,在创建模块管理器(ModuleManager)的时候,在DefaultListenerAggregate类中为模块初始化事件绑定了许多默认的监听器对象(Listener),所以这里一旦触发了这些事件后,相应的监听器对象(Listener)就开始执行。
loadModules是被触发的第一个事件,当该事件被触发后,先注册一个自动装载Module类的装载器(autoload),然后遍历需要处理的模块,对每个模块执行loadModule()方法。在loadModule()方法中,通过调用loadModuleByName()方法触发loadModule.resolve事件,该事件只绑定了一个监听器:ModuleResolverListener,用来实例化每个模块下的Module类,随后触发loadModule事件。
loadModule事件上绑定了众多的监听器对象,用来处理Module类,完成不同的任务,这些监听器在ZF2的官网文档上介绍了比较详细,可以参考一下, 点击查看ZF2官网描述。我这里就按照执行顺序简单描述一下:
AutoloaderListener:实现模块自己的类自动加载器。
ModuleDependencyCheckerListener:检查模块间的依赖关系。
InitTrigger:执行模块的一些初始化设置 。
OnBootstrapListener:如果Module类中有onBootstrap方法,则把该方法绑定到MvcEvent::EVENT_BOOTSTRAP事件上,该事件将在MVC引导阶段被触发。
LocatorRegistrationListener:在ServiceManager对象中注册Module对象。
ConfigListener:把模块自己的配置信息与全局配置信息合并到一起,全局配置信息覆盖模块的配置信息。
最后触发的是loadModules.post事件,它只绑定了一个监听器对象:ServiceListener,主要负责根据配置信息注册服务对象,举个例子比较容易懂。
ZF2默认在全局服务对象(ServiceLocator)中注册了一个名为ControllerLoader的服务对象,该对象也是一个ServiceManager对象,用来存放控制器对象。那么如何在ControllerLoader中注册控制器对象呢?ZF2是这么做的:先在ServiceListener中添加要监控的一些ServiceManager对象,其中就包括ControllerLoader,如下列代码所示。
01
02
03
04
05
06
|
$serviceListener
->addServiceManager(
'ControllerLoader'
,
'controllers'
,
'Zend\ModuleManager\Feature\ControllerProviderInterface'
,
'getControllerConfig'
);
|
这样,当loadModule事件被触发时,ServiceListener就会去执行Module类中的getControllerConfig()方法,并把返回的数据保存起来,当loadModules.post事件被触发时,ServiceListener又会从全局配置信息中寻找名字为controllers的那个配置项,把数据也保存起来,最后把上述的所有配置信息合并成一个。最后,ServiceListener会根据配置信息为ControllerLoader对象创建属于它的ServiceManager对象。在官方示例中,Application模块下的module.config.php中有下面这么一条配置信息,根据这条配置信息,就在ControllerLoader对象中注册了Application\Controller\IndexController控制器对象。
01
02
03
04
05
|
'controllers'
=>
array
(
'invokables'
=>
array
(
'Application\Controller\Index'
=>
'Application\Controller\IndexController'
),
),
|
当初始化阶段完成后,会把所有模块(Module)目录下的ModuleConfig与全局的GlobConfig的内容合并成一个数组,GlobConfig会覆盖ModuleConfig下的同名内容,以Config为名字保存在ServiceLocator对象中,另外在ServiceLocator对象中还有一条为ApplicationConfig的内容,保存的是config/application.config.php的内容。
模块(Module)的初始化,如果用一句话来概括,就是按照每个模块的配置信息,在全局服务对象(ServiceLocator)中注册各个模块。当模块初始化完成后,意味着在程序中所有要用到的服务对象都已准备就绪,接下来就该是这些模块开始工作了。
ZF2已经发布,与ZF1相比,MVC这一模块内部的实现机制可谓大相径庭,许多用过ZF1的PHPer刚接触ZF2会有种无所适从的感觉,同时ZF2的官方手册也不是很详尽,许多细节只有通过看源代码才能搞懂,而ZF2中又大量使用了各类设计模式,使得对设计模式不熟悉的开发者阅读源代码的时候更是一头雾水。下面我就简单的介绍一下ZF2中MVC的主要流程。
ZF2的MVC模块主要采用了Service Locator与事件驱动的设计模式。下面先简单介绍下这2部分内容。
1、Service Locator
在ZF2中,新增了一个Zend\ServiceManager组件,该组件实现了Service Locator模式。在ZF2中,任何的类库,扩展等资源都统一被视为服务对象,并在ServiceManager组件中注册。当应用程序需要某个功能,例如Zend\Log时,推荐的做法是在ServiceManager中获取,而不是像传统方式那样,include类文件之后再new一个对象出来。这种方式的好处在于解除了服务调用者与服务提供者之间的耦合,作为一名用户,不需要知道Zend\Log位置是在哪里,是如何产生的,只需要知道用Zend\ServiceManager::get()这种方式来取得它就行了。
要在ServiceManager中注册服务对象,有下面几种方法:
直接对象:调用setService方法,可以直接把某个对象注册为一个服务对象。
01
02
03
|
$serviceManager
->setService(
'Log'
,
new
Zend\Log\Logger());
$logger
=
$serviceManager
->get(
'Log'
);
$serviceManager
->setService(
'settings'
,
array
(
'password'
=>
'123456'
));
|
延迟对象:调用setInvokableClass方法,其实就是先注册一个类的名字,当需要使用该服务对象时,ServiceManager实例化一个对象给你。
01
02
|
$serviceManager
->setInvokableClass(
'Log'
,
'Zend\Log\Logger'
);
$logger
=
$serviceManager
->get(
'Log'
);
|
工厂对象:调用setFactory方法,有许多服务对象的产生比较复杂,比如根据配置文件的内容来产生对象等等,这时就需要有个专门的“工厂”来产生服务对象。工厂可以是匿名函数,也可以是实现了 Zend\ServiceManager\FactoryInterface的类或对象。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
|
use
Zend\ServiceManager\FactoryInterface;
use
Zend\ServiceManager\ServiceLocatorInterface;
class
LogFactory
implements
FactoryInterface
{
public
function
createService(ServiceLocatorInterface
$serviceLocator
)
{
return
new
Zend\Log\Logger();
}
}
// 实现了Zend\ServiceManager\FactoryInterface
$serviceManager
->setFactory(
'Log1'
,
new
LogFactory());
$serviceManager
->setFactory(
'Log2'
,
'LogFactory'
);
$logger
=
$serviceManager
->get(
'Log1'
);
// 匿名函数
$serviceManager
->setFactory(
'service3'
,
function
() {
return
new
\stdClass(); });
|
抽象工厂对象:如果要取得一个名字不存在的服务对象时,ServiceManager会去寻找注册的抽象工厂,抽象工厂实现了Zend\ServiceManager\AbstractFactoryInterface,如果有某个抽象工厂的canCreateServiceWithName方法返回真,则ServiceManager返回该抽象工厂的createServiceWithName方法所产生的对象。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
use
Zend\ServiceManager\ServiceLocatorInterface;
use
Zend\ServiceManager\AbstractFactoryInterface;
class
MyAbstractFactory
implements
AbstractFactoryInterface
{
public
function
canCreateServiceWithName(ServiceLocatorInterface
$serviceLocator
,
$name
,
$requestedName
)
{
// this abstract factory only knows about 'foo' and 'bar'
return
$requestedName
===
'foo'
||
$requestedName
===
'bar'
;
}
public
function
createServiceWithName(ServiceLocatorInterface
$serviceLocator
,
$name
,
$requestedName
)
{
$service
=
new
\stdClass();
$service
->name =
$requestedName
;
return
$service
;
}
}
$serviceManager
->addAbstractFactory(
'MyAbstractFactory'
);
var_dump(
$serviceManager
->get(
'foo'
)->name);
// foo
var_dump(
$serviceManager
->get(
'bar'
)->name);
// bar
var_dump(
$serviceManager
->get(
'baz'
)->name);
// exception! Zend\ServiceManager\Exception\ServiceNotFoundException
|
服务对象的初始化:ServiceManager还提供了对服务对象的额外初始化功能,调用addInitializer()方法可以注册对象初始化器,每当调用ServiceManager的get()方法返回服务对象前,都会对该对象调用所有注册的初始化器,该功能一般用于对第三方开发的服务对象的初始化。
看到这里,如果有熟悉Java的,特别是Spring框架的同学,可能会发现这ServiceManager与Spring中的Ioc容器有点相似。没错,其实两者都是为了实现一个目的:把对象的生产者与对象的使用者之间的耦合性降到最低。Spring中采用的是依赖注入的模式,而ZF2中原本也打算使用用依赖注入的模式,还专门开发了Zend\Di组件,却在最后一个测试版中换成了ServiceLocator模式。但无论采用哪种模式,最终的目的与产生的效果还是一样的:即降低耦合性。我们在使用ZF2的时候也要注意这一点:对象的产生不应该是由对象的使用者决定的,使用者只是选择采用哪一个对象而已。
2、事件驱动
在ZF2中,大量使用了事件驱动的模式。在ZF2中,提供了一个新的Zend\EventManager组件来实现事件驱动。打开Zend\EventMan ager目录,可以看到很多都是一些接口(Interface),核心的就是两个文件,Event.php和EventManager.php,望文生义,一个是事件类(Event),一个是事件管理类(EventManager)。
先说说EventManager类,EventManager类主要负责绑定(attach)事件、解除(detach)事件、触发事件(trigger),在EventManager类内部,维护着一个事件数组,事件数组维护者事件名称与事件处理函数的一对多的对应关系,也就是说一个事件名称可以绑定多个事件处理对象(函数),当事件被触发后,按照绑定顺序,依次执行所绑定的事件处理对象(函数),事件处理对象(函数)也被称为监听器(Listener)。
举个例子可能来理解起来比较清楚:有个论坛用户发文章的功能,每当保存数据到数据库之前,会触发一个名为preSaveContent的事件,我们可以为该事件绑定多个处理函数,比如检查用户权限的函数,过滤敏感字符的函数,判断IP的函数等等,这些函数会以绑定顺序依次执行。
再说说Event类,Event类实例化后就是Event对象,该对象保存了事件的名称,事件发生时的上下文,需要传递给事件处理函数的一些参数。值得一提的是stopPropagation方法,该方法阻止了事件的继续处理,也就是说执行了该方法后,事件队列中后面的处理函数将不会执行。
在ZF2中预定义了一些不同的事件对象,如ModuleEvent、MvcEvent、ViewEvent、SendResponseEvent等等,他们都继承于Zend\EventManager\Event。这些不同的事件对象所起的作用其实是一样的,都是在事件驱动的工作流程中把事件发生时的上下文信息传递给监听器(Listener),不同之处在于事件发生时的逻辑环境。任何程序在逻辑上总是会有不同的执行阶段的,在不同阶段的上下文环境是不同的,如果用一个事件对象来贯穿所有的执行阶段,必然会在该对象上附加所有阶段的上下文信息,从而导致该对象的臃肿,程序结构也不清晰。因此好的设计方法就是在不同的逻辑环境中,使用不同的事件对象。
下面介绍一下ZF2中MVC的启动过程,在ZF2的官网中,提供了一个例子程序: http://framework.zend.com/manual/2.2/en/user-guide/skeleton-application.html ,这里就用这个例子作为说明对象。
把官网的示例程序导入Zend Studio后,是下图的结构:
其中,vendor目录一般存放第三方开发的或是自己开发的通用的模块(Module),而module目录一般存放当前项目的模块(Module)。在ZF2中,每个Module就是一个完全独立的功能模块,可以拥有自己的配置文件、语言文件等等。
与ZF1一样,入口文件是public目录下的index.php文件,该文件一共就3行代码:
01
02
03
04
05
06
07
08
09
10
11
12
|
<?php
/**
* This makes our life easier when dealing with paths. Everything is relative
* to the application root now.
*/
chdir
(dirname(__DIR__));
// Setup autoloading
require
'init_autoloader.php'
;
// Run the application!
Zend\Mvc\Application::init(
require
'config/application.config.php'
)->run();
|
前两行是设置一个PHP类的自动加载器(autoloading),第三行才是真正的MVC的启动代码。
ZF2在逻辑上把一个PHP项目看作一个服务端程序,而不是分散的页面,所以ZF2在设计MVC框架的时候,在逻辑上把运行过程分为两个步骤:1、各模块(Module)以及服务对象的初始化;2、程序运行。
1、初始化
初始化主要功能就是读取配置文件,根据配置文件中的内容生成一个个服务对象并进行适当的初始化设置、注册各类事件并绑定事件处理对象(函数)。这里有一点要提一下,官方推荐的实例布局中,可以看到配置文件分散在许多地方,其中每个模块(Module)下面都有一个config目录,用来存放模块自己的配置文件,其内容称为模块配置信息(ModuleConfig);而在项目根目录下,有一个config目录,存放的是与整个项目结构(诸如有哪些模块、模块存放在哪里等等)有关的配置文件,其称为应用程序配置信息(ApplicationConfig);而autoload目录下存放的是与项目版本,环境有关的配置文件,他们会覆盖module目录下配置文件的内容,其内容称为全局配置信息(GlobConfig),其中goloal.php一般存放与发布的版本有关的配置信息,例如版本号等,而local.php一般存放与环境有关,涉及安全方面的一些配置信息,如数据库的密码等。
初始化执行的是Zend\Mvc\Application的init()方法,一共五行代码,完成的工作可不少,我们一行行看。第一、二行读取配置文件中service_manager和listerners的内容,这里先略过,因为示例代码也没有用到这两部分配置,所以我们这里也不去关注它们,等对ZF2熟悉了以后,自然就能搞明白了。
01
02
|
$serviceManager
=
new
ServiceManager(
new
Service\ServiceManagerConfig(
$smConfig
));
$serviceManager
->setService(
'ApplicationConfig'
,
$configuration
);
|
如上图,接下来是产生一个ServiceManager对象,上面说过了,ZF2用到了Service Locator模式,程序中用到的服务,对象甚至全局参数都可以从ServiceManager中获取,这里生成的ServiceManager对象可以称它为全局ServiceManager对象(或者称之为ServiceLocator对象)。为了方便在程序中获取ServiceLocator对象,ZF2提供了一个ServiceLocatorAwareInterface接口,凡是实现了ServiceLocatorAwareInterface接口的类,都能通过调用getServiceLocator()方法来取得ServiceLocator对象。
ServiceManager可以有许多个,一般按照存放的内容不同来分类,例如专门存放数据库对象的ServiceManager,或专门存储日志对象的ServiceManager,而ServiceManager对象本身又可以保存在全局的ServiceManager对象中。
ZF2中就是这么做的,一个全局的ServiceManager对象中还保存其他的如ControllerPluginManager、ViewHelperManager、ValidatorManager之类的ServiceManager对象,就拿ControllerPluginManager来说,它保存的是各种控制器插件,ZF2提供了许多的控制器插件,如果把这些控制器插件对象都保存在全局的ServiceManager对象中,会显得有点臃肿杂乱,所以在全局的ServiceManager对象中就保存了一个ControllerPluginManager对象,而在ControllerPluginManager对象中保存那些五花八门的控制器插件,获取的时候,就可以这么获取:
01
|
$serviceManager
->get(
'ControllerPluginManager'
)->get(
'redirect'
);
|
ServiceManager对象初始化的时候,默认注册了两个工厂,分别是事件管理器工厂(EventManagerFactory)和模块管理器工厂(ModuleManageFactory),是在Zend\Mvc\Service\ServiceManagerConfig中注册的,后面我们会看到在另一个类中注册了许多其他的服务对象。事件管理器工厂比较简单,仅仅创建一个事件管理器(EventManager)对象,该对象负责整个MVC流程中所有事件的绑定以及触发。而模块管理器工厂比较复杂,在创建模块管理器(ModuleManager)对象时,还绑定了模块初始化过程中的一些默认事件监听对象(Listener)。
在程序初始化阶段,有一个重要的部分就是各模块(Module)的初始化,模块(Module)初始化过程中,有四个事件(模块初始化事件)会被触发,依次是:loadModules(开始载入所有模块),loadModule.resolve(单个模块开始解析),loadModule(单个模块开始载入),loadModules.post(所有模块载入完成),以上这些事件定义在Zend\ModuleManager\ModuleEvent类中。在ZF2中,为这些事件绑定了许多默认的监听器(Listener),大多数监听器的绑定由Zend\ModuleManager\Listener\DefaultListenerAggregate类完成,从名字就可看出,它是一个默认监听器的聚合类。
另外还绑定了一个Zend\ModuleManager\Listener\ServiceListener监听器,该监听器是独立绑定的,并不包括在DefaultListenerAggregate内。ServiceListener是个比较复杂的监听器,它主要合并各模块(Module)的配置信息,并根据配置信息注册服务对象。ServiceListener监听器是由ServiceListenerFactory产生的,ServiceListenerFactory在产生ServiceListener监听器后,还为它设置了许多默认的与MVC有关的服务对象。那么为什么这些服务对象不在Zend\Mvc\Service\ServiceManagerConfig类中注册呢?因为这些对象中用到的许多参数是可配置的,由ServiceListener把配置信息中的相关数据传递给它们。
模块管理器(ModuleManager)负责管理所有的模块(Module),具体有哪些模块需要处理,则是在配置文件中写明的,在application.config.php文件返回的配置信息数组中,有一项名为modules的配置选项就是告诉模块管理器有哪些模块需要被处理。
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
return
array
(
'modules'
=>
array
(
'Application'
,
'Album'
,
'Blog'
,
),
'module_listener_options'
=>
array
(
'config_glob_paths'
=>
array
(
'config/autoload/{,*.}{global,local}.php'
,
'module_paths'
=>
array
(
'./module'
,
'./vendor'
,
),
),
)
|
每个模块下都必须有个Module类,该类用来配置当前模块。
当ServiceManager对象产生并初始化之后,接下去就该是各模块(Module)的初始化了:
01
|
$serviceManager
->get(
'ModuleManager'
)->loadModules();
|
模块(Module)的初始化,是由ModuleManager触发相关事件(上文提到的模块初始化事件)后完成的。这是ZF2中事件驱动机制的第一次运作,后面将会贯穿程序运行的整个生命周期。
下面看看模块(Module)的初始化究竟做了哪些事情,首先从ServiceManager中取出模块管理器(ModuleManager),然后执行loadModules()方法,该方法很简单,就是触发loadModules与loadModules.post事件。上面说过,在创建模块管理器(ModuleManager)的时候,在DefaultListenerAggregate类中为模块初始化事件绑定了许多默认的监听器对象(Listener),所以这里一旦触发了这些事件后,相应的监听器对象(Listener)就开始执行。
loadModules是被触发的第一个事件,当该事件被触发后,先注册一个自动装载Module类的装载器(autoload),然后遍历需要处理的模块,对每个模块执行loadModule()方法。在loadModule()方法中,通过调用loadModuleByName()方法触发loadModule.resolve事件,该事件只绑定了一个监听器:ModuleResolverListener,用来实例化每个模块下的Module类,随后触发loadModule事件。
loadModule事件上绑定了众多的监听器对象,用来处理Module类,完成不同的任务,这些监听器在ZF2的官网文档上介绍了比较详细,可以参考一下, 点击查看ZF2官网描述。我这里就按照执行顺序简单描述一下:
AutoloaderListener:实现模块自己的类自动加载器。
ModuleDependencyCheckerListener:检查模块间的依赖关系。
InitTrigger:执行模块的一些初始化设置 。
OnBootstrapListener:如果Module类中有onBootstrap方法,则把该方法绑定到MvcEvent::EVENT_BOOTSTRAP事件上,该事件将在MVC引导阶段被触发。
LocatorRegistrationListener:在ServiceManager对象中注册Module对象。
ConfigListener:把模块自己的配置信息与全局配置信息合并到一起,全局配置信息覆盖模块的配置信息。
最后触发的是loadModules.post事件,它只绑定了一个监听器对象:ServiceListener,主要负责根据配置信息注册服务对象,举个例子比较容易懂。
ZF2默认在全局服务对象(ServiceLocator)中注册了一个名为ControllerLoader的服务对象,该对象也是一个ServiceManager对象,用来存放控制器对象。那么如何在ControllerLoader中注册控制器对象呢?ZF2是这么做的:先在ServiceListener中添加要监控的一些ServiceManager对象,其中就包括ControllerLoader,如下列代码所示。
01
02
03
04
05
06
|
$serviceListener
->addServiceManager(
'ControllerLoader'
,
'controllers'
,
'Zend\ModuleManager\Feature\ControllerProviderInterface'
,
'getControllerConfig'
);
|
这样,当loadModule事件被触发时,ServiceListener就会去执行Module类中的getControllerConfig()方法,并把返回的数据保存起来,当loadModules.post事件被触发时,ServiceListener又会从全局配置信息中寻找名字为controllers的那个配置项,把数据也保存起来,最后把上述的所有配置信息合并成一个。最后,ServiceListener会根据配置信息为ControllerLoader对象创建属于它的ServiceManager对象。在官方示例中,Application模块下的module.config.php中有下面这么一条配置信息,根据这条配置信息,就在ControllerLoader对象中注册了Application\Controller\IndexController控制器对象。
01
02
03
04
05
|
'controllers'
=>
array
(
'invokables'
=>
array
(
'Application\Controller\Index'
=>
'Application\Controller\IndexController'
),
),
|
当初始化阶段完成后,会把所有模块(Module)目录下的ModuleConfig与全局的GlobConfig的内容合并成一个数组,GlobConfig会覆盖ModuleConfig下的同名内容,以Config为名字保存在ServiceLocator对象中,另外在ServiceLocator对象中还有一条为ApplicationConfig的内容,保存的是config/application.config.php的内容。
模块(Module)的初始化,如果用一句话来概括,就是按照每个模块的配置信息,在全局服务对象(ServiceLocator)中注册各个模块。当模块初始化完成后,意味着在程序中所有要用到的服务对象都已准备就绪,接下来就该是这些模块开始工作了。
2、程序运行
当模块(Module)初始化结束后,就可以进入程序的正式运行阶段了:上文提到过,ZF2在逻辑上把一个Web项目看作一个服务端的程序,而不是各个分散的页面,因此ZF2提供了一个Zend\Mvc\Application类来实现这个逻辑,这样一来,各类的HTTP请求在ZF2中就被看作是Application中接收到的一个消息而已。我们接着往下看:
01
|
$serviceManager
->get(
'Application'
)->bootstrap(
$listeners
);
|
上述代码先取出一个已经注册过的Application对象,该对象由Zend\Mvc\Service\ApplicationFactory创建。在Application对象创建的时候,其内部创建了一个Request对象和一个Response对象,分别表示Application从外界收到的请求与发出的回应。然后执行bootstrap()方法。
bootstrap,中文意思是引导,顾名思义,进入到引导阶段,说明程序马上要运行了。因为程序运行是基于MVC模式的,因此引导阶段的主要任务就是为MVC的正常工作做准备。ZF2中的MVC模式按照生命周期看,分为四个部分:路由解析(route resolve)、调度(dispatch)、渲染视图(render view)、发送响应(send response),ZF2中也为之定义了相关的事件名称。
在模块初始化阶段,事件对象为ModuleEvent,那么在MVC工作阶段,事件对象就是MvcEvent了,而MvcEvent对象的初始化,自然就放在引导阶段了。在bootstrap()方法中,先为MVC的相关事件绑定监听器,然后产生MvcEvent对象并设置了上下文环境以及Request、Response、Router三个对象,最后触发在MvcEvent对象上的MvcEvent_EVENT_BOOTSTRAP事件,下面就详细描述一下这些动作内部的过程。
1) 绑定监听器
之前已经多次强调,ZF2是基于事件驱动的,所以MVC的整个工作流程就是触发不同阶段的MVC事件,然后由系统回调绑定的监听器(事件处理方法)。ZF2提供了贯穿整个MVC过程的默认的监听器,这些监听器被集合在四个类中,分别是RouteListener、DispatchListener、ViewManager、SendResponseListener。由于MVC事件中绑定了许多的监听器,为了方便大家了解,ZF2的官方文档中详细罗列了这些事件绑定的监听器以及触发条件和触发位置,具体内容请看: http://framework.zend.com/manual/2.2/en/modules/zend.mvc.mvc-event.html
RouteListener:为route事件绑定了一个监听器——>RouteListener::onRoute(),该监听器用来解析HTTP请求中的URL地址(严格说是REQUEST_URI域),如果解析失败触发dispatch.error事件。
DispatchListener:为dispatch事件绑定了一个监听器——>DispatchListener::onDispatch(),该监听器根据解析后的URL地址信息找到对应的控制器(Controller)对象,然后执行对应的动作方法(Action)。
ViewManager:ZF2中的视图是个相当复杂的组件,涉及到一堆概念:变量的容器(Variables Containers)、视图模型(View Models)、视图助手(Helper)、模板(Template)、渲染策略(Render Strategy)、脚本解析(Reslover)、响应策略(Response Strategy),ZF2还专门定义了一个视图事件(ViewEvent)的类来参与对视图部分的事件处理。因此在ViewManager类中为各类事件绑定了众多的与视图处理有关的监听器,具体作用到了后面视图部分会专门叙述,这里就不详说了。
SendResponseListener:为finish事件绑定了一个监听器——>SendResponseListener::sendResponse(),该监听器用来产生ResponseEvent对象,并在该对象上触发一系列与发送相应请求(Response)有关的事件。
2) 触发MvcEvent上的bootstrap事件
与MVC事件相关的监听器绑定完毕后,开始触发MvcEvent对象上的第一个事件MvcEvent_EVENT_BOOTSTRAP。该事件触发后,会去检查各个模块(Module)下Module类中的onBootstrap()方法,如果有就执行。另外在ViewManager中也有响应该事件的onBootstrap()方法,主要用来为其他事件绑定监听器,用来处理视图方面的内容。
当引导程序执行完毕后,意味着与MVC相关的组件都已准备就绪了,之后调用Zend\Mvc\Application::run()方法,开始运行程序。run()方法其实很简单,主要就是触发了四个事件:MvcEvent::EVENT_ROUTE、MvcEvent::EVENT_DISPATCH、MvcEvent::EVENT_RENDER、MvcEvent::EVENT_FINISH。绑定在这些事件上的监听器会负责完成MVC的整个流程。
首先触发的是MvcEvent::EVENT_ROUTE事件,从名字上就可以看出,该事件触发后,会完成路由解析的工作。该事件绑定了两个监听器(见上图),主要功能集中在Zend\Mvc\RouteListener这个监听器上。在监听器中,先从MvcEvent对象中取出路由对象(router),随后执行match()方法,如果当前的HTTP请求中的REQUEST_URI域(就是请求的URL地址)有与之相配的路由,则返回一个Zend\Mvc\Router\RouteMatch对象,并把它在MvcEvent对象中保存一份,以方便后续程序的使用。其中match()方法我们可以不用去关心,只要知道路由成功匹配后会返回一个RouteMatch对象就行了。重点是路由对象(router)的产生,路由对象(router)是Zend\Mvc\Router\Http\TreeRouteStack的实例,在引导阶段(bootstrap)产生,与其他大多数的对象一样,它也是一个服务对象(注册在Service Locator中,由Zend\Mvc\Service\RouterFactory产生), 所以也是从全局的ServiceManger对象中获取它。
在RouterFactory中,我们可以发现它会去配置信息中寻找一个名为route_plugin的项,如果没提供该配置项的话,默认会把Zend\Mvc\Router\RoutePluginManager作为该项的内容。鉴于这个route_plugin所指向的内容在路由中占据了重要地位,因此先在这里介绍一下它,明白了它所起的作用,就等于了解了路由的内部实现机制。
路由,说到底就是一种映射关系,在ZF2中定义了一些通用的映射方式,并封装成了类,称之为route,一个route其实就是一个路由协议(这里请注意与路由对象——router的区别),路由对象(router)就是用这些route来与HTTP请求中的URL地址作匹配的,而这些route在路由对象(router)内部是由一个名为routePluginManager的对象负责产生的,该routePluginManager对象就由route_plugin所指定,必须为Zend\Mvc\Router\RoutePluginManager的子类。仔细看下RoutePluginManager的继承关系,发现它是Zend\ServiceManager\ServiceManager的子类,这下明白了,所谓的RoutePluginManager,其实就是一个服务对象管理器,而route则是注册在RoutePluginManager的服务对象,当然RoutePluginManager本身又是在全局服务对象(Service Locator)中注册的。有了routePluginManager,路由对象(router)就可以根据配置文件中的信息,生成一个个route对象了。
定义route对象的配置信息一般如下:
我们可以看到,配置信息非常复杂,这是很多人的第一印象,但是当我们理解了这些配置信息背后的含义之后,会明白其实是很有规律可循的,也是用起来相当简单的。我们从第二行开始讲起,第二行的router表示指向的数组是路由配置信息,这个router是默认名字,不可改变。第三行的routes表示指向的数组是路由表项的配置信息,这个routes也是默认名字,不可改变。路由表项可以有任意个,每一个描述了一个路由规则,我们这里定义了两个路由表项,分别取名为home与application。
home路由表项的配置比较简单,共有2个参数——type与options,type参数指定了该路由表项使用的是哪个路由协议,路由协议必须是在RoutePluginMnager中注册过的,而options参数是该协议要用到的参数。home路由表项中type指向的是Zend\Mvc\Router\Http\Literal协议,options指向的是该协议用到的两个参数route与defaults,在Literal协议中,route表示匹配的路由模式,而defaults中的内容表示相应的处理方法。以home路由表项为例,当用户的请求地址为项目根地址时,就执行Application\Controller\Index控制器中的indexAction方法。这里有一点要注意,controller指向的必须是一个在ControllerManager中注册过的控制器,否则就返回ERROR_CONTROLLER_NOT_FOUND的错误。