Skip to content

Toturial.Pi Engine Mechanism.zh_cn

linzongshu edited this page Jul 4, 2013 · 1 revision

English

在基础部分中,我们已经介绍过Pi Engine的一些基本信息,以及简单介绍下了Pi Engine的运行机制,在这一章中,我们将会结合Pi Engine的源码更详细地介绍下整个Pi框架的运行机制,Pi Engine如何从一个URL请求开始,经过中间处理后最终将结果发回给浏览器端。

之前我们已经提到过,Pi Engine的整个运行过程主要分为数据初始化和触发事件两部分,这里将详细介绍每一个运行过程中要完成的任务。

  1. 数据初始化

数据初始化就是将系统的配置,请求所需要的数据写入变量中,以便后续系统运行时调用这些变量值,生成页面。初始化的流程在基础篇的第一章里已经介绍地比较清楚了,大致的流程也不再赘述,这里将会结合源码,并按初始化的顺序对一些比较重要的机制作详细介绍。

1.1 初始化host

初始化host是通过Pi类里的host()方法实现的,这个方法是一个静态方法,它负责实例化一个Pi\Application\Host对象,并将其赋给静态变量$host,由于变量定义为静态,因此之后每次调用host函数都会返回同一个Pi\Application\Host实例,这也保证了数据的一致性。这种方式也就是一种单例模式。而在构造Pi\Application\Host时,则将var/config/host.php里的数据写入这个类的变量里(如$path)。因为这是一个单例模式,虽然$path变量不是静态的,但之后的调用都会读取这个$path的值。

lib/Pi.php

...
public static function host($config = null)
{
    if (!isset(static::$host)) {
        if (!class_exists('Pi\\Application\\Host', false)) {
            require static::PATH_LIB . '/Pi/Application/Host.php';
        }
        static::$host = new Pi\Application\Host($config);
    }
    return static::$host;
}
...

之后在任何地方调用下面代码,都会得到一样的结果:

Pi::host()->get('path');

基本上整个Pi系统在初始化的时候都采用这种机制,通过这种方式将请求所需要的资源集合起来,之后在需要的时候通过调用类所提供的访问变量的API即可。

1.2 自动加载及命名空间

我们在引用一个类的时候必须先用require或include等包含该类的文件路径,当然我们不可能对所有需要引用的类手动include,这一步的作用就是利用php的spl_autoload_register函数来自动include未定义的类。同时在这里也对命名空间进行了处理。

在PHP5.3以后开始支持命名空间,这也为开发人员提供了很大的方便。因为在PHP里规定,在同一个进程中是不允许有相同名字的类,即使它们在不同的文件夹下,这样就迫使开发人员不得不花更多的精力去保证工程里没有重名的类,但这对于大型的开发工程几乎是不可能的。而命名空间很好地扮演了解决代码冲突的角色,同时命名空间为很长的标识符名称创建一个别名,提高源代码的可读性。

这里的初始化实际上实例化了一个Pi\Application\Autoloader类,在这个类构造的时候调用自己的register()方法,这个方法里指定了对于未定义的类要用哪个方法来处理:

Pi\Application\Autoloader.php

public function register()
{
    ...
    // Register PSR rule map autoloader
    spl_autoload_register(array($this, 'autoloadStandard'));
}

而autoloadStandard这个函数就是用来处理未定义的类,它对未定义的类的命名空间的第一个字符进行解析,如果是Module,则是模块里的类,相应地生成模块里的文件路径。如Module\Demo\Controller\Admin\IndexController则对应于路径usr/module/demo/src/Controller/Admin/IndexController.php。而如果是Zend或者Pi则分别生成这两个目录下的路径,最后将这个路径include。

1.3 加载服务

上面完成了系统的初步初始化,之后Pi前端控制器将执行权交给Pi\Application\Engine\Standard句柄,由它来执行后续的初始化和运行。加载服务也就在这里进行。

Pi/Application/Engine/Standard.php

public function bootstrap()
{
    // Load Pi services
    $status = $this->loadService();
    if (false === $status) {
        return false;
    }

    // Load application, which could be called during resouce setup
    $application = $this->application();

    // Load application resources
    $status = $this->setupResource();
    if (false === $status) {
        return false;
    }

    // Boot application
    $application->bootstrap();

    return $this;
}

需要加载哪些服务是在var/config/application.front.php,application.admin.php或application.feed.php里定义的。目前只有在front.php里定义了log资源,这个资源的作用就是将请求过程中产生的一些重要的调试信息输出给开发人员看,信息在页面的最下面。用户可以自定义想要加载的服务,这里虽然定义加载的服务只有log,但大多数服务在后面加载资源的时候也都会被加载。

fig11-1

图11-1 Pi Engine的debug模式

服务的加载机制是通过Pi的静态方法Pi::service()实现的,该方法所带的参数将决定实例化哪个句柄来加载服务,如Pi::service('log')就实例化Pi\Application\Service\Log,而在实例化的过程中,这个服务所需要的数据都被初始化并保存到变量里。

调用服务加载方法Pi::service()时,程序会去var/config目录下去查找相关的服务配置,这些配置文件以service.{service name}.php的命名形式,比如Pi::service('log'),将会加载var/config/service.log.php文件,而这个配置文件里就定义了是否开启解析器(profiler)、debug环境下哪些错误类型需要提示等。这些配置信息将会保存在Pi\Application\Service\Log类中的$options变量里,在需要的时候就会被调用,而Pi::service()也是一个单例模式,所以之后在任何地方调用Pi::service('log')都会指向同一个Pi\Application\Service\Log类。

对于系统默认的服务以及服务的作用,我们将在加载资源一节中讲。

1.4 初始化应用数据

在加载完服务后,engine句柄将会初始化应用数据,这些数据将在加载资源的时候被用到,这些应用数据主要包括:

  • 服务管理句柄(Zend\ServiceManager\ServiceManager)
  • 系统配置
  • 事件驱动句柄(Zend\EventManager\EventManager)
  • 请求信息(Zend\Http\PhpEnvironment\Request)
  • 返回信息(Zend\Http\PhpEnvironment\Response)

服务管理句柄其实对服务管理器(ServiceManger)的实例化,服务管理器是Zend 2.0新引入的一个概念,它是对“服务定位模式(Service locator pattern)”编程思想的实现。这种思想提倡将程序中的每一个独立功能提取出来作为一个“服务”,每一个服务都是独立可唤醒的,只有服务被调用时,服务相关的程序才会启动。比如我们后面会细说的系统配置、请求信息以及返回信息都是独立成一个“服务”,同时也由服务管理句柄来实例化相关的对象。在这里实例化Zend\ServiceManger\ServiceManager主要是将Pi Engine需要的一些服务配置与Zend默认的合并,同时在之后能通过这个句柄实例化其他服务对象。Pi可能需要的服务在Pi\Mvc\Service\ServiceManagerConfig里定义,分别用$invokables,$factories,$aliases定义,比如在$invokables里有:

'Config'  => 'Pi\Mvc\Service\Config',

调用下面代码,将会实例化Pi\Mvc\Service\Config对象:

$serviceManager->get('Config');

服务管理句柄的获取方式为:

Pi::engine()->application()->getServiceManager();

系统配置就是实例化Pi\Mvc\Service\Config对象。

事件驱动句柄是事件驱动(EventManager)的实例化。传统的程序中,代码都是按线性顺序执行的,所以开发中往往很难将一些功能独立为一个组件或模块。事件驱动(或者也可以叫钩子--Hook),改变了普通框架MVC流程化的运行方式,应用了事件驱动后,程序将呈现“注册事件”到“触发事件”的跳跃式运行,可以在不影响原有程序代码的情况下,很容易的任意位置加入新的业务逻辑,让项目的开发变得极为灵活[参考百度百科]。在Zend机制里主要有这几种事件类型:bootstrap(引导),route(路由),dispatch(分发),render(渲染)和finish(结束)。在Zend里这些事件触发的顺序依次为bootstrap -> route -> dispatch -> render -> finish。而独立的业务逻辑(也就是一个方法或函数)都会按优先级在每个事件里先注册好,当触发该事件时,EventManager就是根据注册的方法或函数的优先级依次执行。这种机制为系统的升级提供了很大的方便,开发者可以在加入新的业务逻辑的同时而不去更改系统的主体代码。

Pi Engine将初始化的事件驱动句柄保存在Pi\Mvc\Application类的$events变量里,调用的方法为:

$eventManager = Pi::engine()->application()->getEventManager();

而注册事件和触发事件也非常简单,比如当前类有个loadTranslator函数需要注册:

$eventManager->attach('dispatch', array($this, 'loadTranslator'));

由于dispatch事件已经是Zend默认的事件,因此在其流程里已经被触发,开发人员就不需要再添加触发代码了。

请求信息就是一个请求里所包含的环境参数、get参数、post参数、上传文件信息、服务器信息、header信息以及Cookies等,这些信息都保存在Zend封装好的类-- Zend\Http\PhpEnvironment\Request中。Pi在初始化时会实例化该对象,同时将这些数据从发送过来的请求里提取出来并写入变量里。这些数据在之后做进一步的请求处理将会用到,比如获取post数据可以这样访问:

Pi::engine()->application()->getRequest()->getPost();

返回信息是Zend\Http\PhpEnvironment\Response的实例,这个类主要用于处理请求完成后返回的Http响应,比如状态码、版本、Headers以及主体内容等。Pi在这里初始化这个数据,之后就可以用它来处理请求的结果。Response实例的获取方式为:

Pi::engine()->application()->getResponse();

1.5 加载资源

Pi Engine在var/config/application.front/admin/feed.php里定义了需要加载的资源,系统在加载资源时可能会根据情况启动相应的服务。加载每一项资源都会实例化与资源相应的对象,相应的对象都在Pi\Application\Resource目录中定义,比如acl资源就对应Pi\Application\Resource\Acl对象,这些实例化后的对象将会保存在Pi\Application\Engine\Standard的$resources变量里。资源所需要的配置也在var/config中相应的文件里定义,一般以resource.{resource name}.php的形式出现,比如security资源就对应resource.security.php文件,文件里的配置信息将会被写到变量里。

资源的加载与服务不一致,服务的加载只是提取一些数据并初始化,而资源的加载实际上已经开始了部分的逻辑处理。下面我们将对系统主要的资源作介绍。

  • Security

这个资源主要用于保证系统的安全,比如禁止访问的IP等,它将调用Pi封装好的Pi\Security类对访问进行处理,当安全检查没有通过则立即停止接下来的运行,资源的配置可以在var/config/resource.security.php里设置。

  • Database

这个资源主要用于加载数据的连接配置,如表前缀、系统表前缀、连接信息等。它将会调用database服务来完成数据库配置的初始化。

  • Router

Router资源会创建一个处理路由的对象--Pi\Mvc\Router\Http\TreeRouteStack,并将其加载到服务管理器中,当然它同时会从数据库里读取所有路由信息并写入变量中,在后面的URL解析中将会用到。

  • I18n

I18n资源将会调用I18n服务初始化系统的本地化(语言翻译)相关的对象-- Pi\I18n\Translator\Translator,这个对象封装了本地化的所有方法。之后还会注册loadTranslator函数,在dispatch的时候会调用。

  • Session

Session资源主要是实例化Pi\Session\Manager对象,用于操作session,同时这一步也会开启session。后面的程序里就可以使用session了。

  • Authentication

Authentication资源的加载主要是通过authentication服务唤醒权限。

  • User

User资源主要是获取当前登陆的用户帐号,并实例化Pi\Application\User对象来关联这个帐号,同时将这些数据写入缓存中。

  • Module

Module资源将setup方法注册到dispatch事件里,这个方法完成从数据库里读取模块主题数据,并使模块主题生效。

1.6 注册事件

将大部分数据初始化完成后,系统就开始将之后需要运行的方法注册到事件驱动器里。之前我们已经提过,系统默认的事件有bootstrap,route,dispatch,render以及complete,因此这部分就是将相关的方法注册到这些事件里。

  • Route

Route事件,顾名思义就是注册那些跟路由相关的方法,这个事件在程序运行时,在除bootstrap事件之外的所有事件之前触发,这是因为Pi采用的MVC的思想,一般一个请求就对应于一个controller/action,而通过解析URL一般会得到controller/action信息以及请求传递过来的参数值,因此需要先解析出这些数据,在dispatch阶段,程序才能知道调用哪个controller/action,并为其传递什么参数。 程序在route事件里只注册了Zend\Mvc\RouteListener::route()方法。这个方法主要用于确定用哪个路由来解析URL,并实例化相关的对象来处理解析后的值。在运行程序一节里我们将会详细介绍这个函数如何实现路由的解析。

  • Dispatch

Dispatch事件是MVC架构里最重要的一个事件,中文译名为分发,在Zend里一般也被叫做前端控制器。这个事件处理的就是整个框架的核心业务逻辑,如生成业面所需数据的处理(一般为M)、Controller/action的调用以及最终的页面布局(View)等。当然Pi Engine也加入一些自己需要的业务,比如页面缓存的处理、模块配置、以及本地化的初始化等。

  • Bootstrap

Bootstrap事件在所有事件里是最先被触发的,在Zend 2.0里由于引入了服务管理器的概念,因此弱化了bootstrap的过程,也加快了程序的启动。在初始化bootstrap事件时,主要是向这个事件里注册了Pi\Mvc\View\Http\ViewManager::onBootstrap()函数并将其优先级设置为最高。这个函数主要是向dispatch和dispatch_error事件里注册页面没有找到、异常发生和页面禁用等处理函数,这些函数将在触发dispatch和dispatch_error事件时会根据优先级顺序执行。

完成所有事件的注册后,系统将会把驱动事件时所需要的数据都写入新实例化的对象--Zend\Mvc\MvcEvent里,如request句柄,response句柄以及treeRouteStack句柄等。这些句柄在之后执行注册函数时会用到。

  1. 触发事件

在上一节的介绍中,Pi Engine基本上把所有初始化的工作都完成了,接下来Pi Engine将开始依次触发所有事件,下图是Pi Engine触发事件的逻辑流程图。

fig11-2

图11-2 Pi Engine事件触发流程图

整个流程从触发route事件开始,在之前的章节里,我们已经提到过,controller/action的解析以及请求传递的参数值都需要从URL里解析出来,因此route事件必须要最先触发。在解析URL时,如果都没有找到合适的路由,将会触发dispatch_error事件,并输出请求错误的页面。当匹配到路由后,dispatch事件将会被触发,页面数据的处理也将在这一步完成。

在dispatch事件,处理缓存相关的函数checkpage()将首先被执行,这个函数会判断当前请求的页面是否设置了缓存,如果有缓存则取出缓存作为最终的内容输出,之后就不需要再去调用controller/action,因此这个注册方法需要最高的优先级。接下来执行的setup()注册方法将会从数据库读取出所有模块配置并存入类变量,同时对前台模块设置主题,也就是指定主题包及模板文件。loadTranslator()注册方法将完成本地化相关对象的初始化。这个注册方法完成之后,也就是最重要的controller/action的执行,在controller dispatch里将会执行相关的action,也就是处理数据,同时会将处理结果保存在ViewModel类里,当然如果调用没有成功,同样将会触发dispatch_error事件。

render事件主要是将处理的数据通过模板转换成HTML标签,而导航与区块的生成也都在这一步里完成。而finish事件里主要将结果保存到缓存里,同时将处理结果输出到浏览器里。

2.1 路由机制

在初始化的时候,系统初始化了Pi\Mvc\Route\Http\TreeRouteStack句柄,并将保存在数据库里的所有路由数据写入了这个句柄的私有变量里。

在数据库保存的路由信息中,用type字段保存了路由的文件,一般有两种形式:直接写类型名以及完整的命名空间。类型名实际上就系统默认的路由,如Feed, Home, Standard, User,因为这部分的路由是系统提供的,所以它们的所在的类文件位置也是已知的,不需要再指定命名空间了,比如User类型用在系统用户注册和登陆页面,其文件位置为Pi\Mvc\Router\Http\User,而Standard就是系统的默认路由类型,它一般组装和解析{controller}/{action}/{key1}/{value1}/{key2}/{value2}形式的URL。而命名空间形式的类型一般就是模块里定义的路由,采用命名空间,在URL解析时就能找到该路由文件了。

在onRoute函数里,包含所有路由信息的变量将首先被读取,程序会根据每个路由的优先级依次读取每个路由的信息,解析出路由所在的类,然后调用类的match函数,在match函数里URL将会被解析出一个个参数值,如模块名、controller、action或其他参数。当然在解析的过程中,程序会判断这个URL的规则是否符合该路由match函数里所定义的,如果不符合,将会返回一个空值,程序就明白这个路由不适合,然后会继续查找下一个路由进行解析,直到路由的match函数返回Zend\Mvc\Router\Http\RouteMatch对象,说明路由已经找到,程序会保存这个对象,同时保存路由对象。而解析出来的参数都保存在RouteMatch对象里。

2.2 MVC

在route事件里,通过执行相关的注册方法,我们已经得到用户要请求的模块(module)、控制器(controller)、动作(action)和其他请求参数值。而dispatch事件主要用于实现MVC架构,解析出来的参数也将在这里被用来定位模块、控制器和动作。

目前的Pi系统里,Dispatch事件主要执行缓存操作、设置模块主题、加载翻译文件以及调用动作方法等一系列注册事件,前三个在流程介绍里已经介绍过了,这也是Pi系统扩展的功能,这里就不再赘述了。而调用动作机制沿用了Zend的处理机制,这里我们主要介绍这部分的流程。

  • 获取Pi的ControllerManager

MVC里的C,也就是控制器,在程序实现上其实就是一个类,而用户请求的页面最终通过控制器类里的动作(action)来实现。在Zend里,对控制器的操作都交给ControllerManager句柄。

Pi系统在dispatch事件里注册了Zend\Mvc\DispatchListener::onDispatch()方法来实现整个MVC流程。在onDispatch方法里,首先创建了一个ControllerLoader服务,需要实现这个服务的类在初始化服务管理器句柄的时候已经定义过了,也就是Pi\Mvc\Service\ServiceManagerConfig这个类里,在这个类里,ControllerLoader服务对应的类为Pi\Mvc\ControllerLoaderFactory,而且这个服务为工厂服务(factory),因此Pi\Mvc\ControllerLoaderFactory::createService方法将被调用。这个服务将返回Pi\Mvc\Controller\ControllerManager实例。这样Pi通过对Zend的类的继承,将操作权交接给自已,这也方便后续功能的更改与扩展。

  • 实例化控制器类

获取到ControllerManager对象后,Pi就可以根据用户的请求实例化相应的控制器类。由于ControllerManager最终继承自ServiceManager,因此它将控制器作为一个服务,并实例化这个对象:

$controller = $controllerLoader->get($controllerName);

在之前我们已经说过,在实例化服务对象时,需要先初始化在哪个类里完成服务对象的实例化。但这个初始化过程在创建服务管理器句柄的时候已经完成,这里如何在初始化呢?原来在ServiceManager类里有个canCreate()方法,用于确定是否能创建这个服务实例,它会调用canonicalizeName()方法将服务名归一化为服务管理器可识别的别名,Pi Engine通过在ControllerManager里对这个方法的覆盖,将控制器初始化为invokable类型的服务,同时根据路由里解析出来的信息,将控制器名转化为相应的类。也就是将控制器指向Pi所定义的目录里。

  • 执行动作

动作的执行是通过触发注册事件完成的,Pi通过使Pi\Mvc\Controller\ActionController继承自Zend的Zend\Mvc\Controller\AbstractController,程序就可以执行Pi的程序来触发注册事件,也就是Pi\Mvc\Controller\ActionController::onDispatch()。在这个函数里,动作名将会被解析成{action name}Action的方法名,之后该控制器类里的相应方法将会被执行,也就是页面请求的所有数据将会在这个方法里完成,同时这个方法将会实例化一个Zend\View\Model\ViewModel类来保存处理后的数据,这个类最终会保存在EventManager的变量里,在之后使用模板时将会被用到。

如果执行失败,程序将会触发dispatch_error事件,完成错误页面的处理及输出。

2.3 渲染模板

模板的调用是通过触发render事件完成的,即Pi\Mvc\View\Http\DefaultRenderingStrategy::render()。在render方法,程序会判断当前请求是否为AJAX请求或者请求是否出错,并为其设置相应的模板。之后,Pi将会创建一个ViewRenderer的服务,也就是实例化Pi\View\Renderer\PhpRenderer对象,接下来模板渲染的工作都交给这个句柄操作。

模板文件路径的解析和最终HTML页面的生成都在Zend\View\Renderer\PhpRenderer的render()方法里实现。在这个方法里,程序会调用ViewModel类里保存的用户设置的模板信息,然后调用Pi\View\Resolver\ModuleTemplate的resolve方法解析出模板文件的完整路径,通过调用include方法去执行模板文件,从而生成最终的页面。

Clone this wiki locally