[译]Zend Framework 1 框架是如何被启动的?

Zend Framework 1 是一个十年前的老框架了,我接触它也有两年了,现在来写这篇文章,主要原因是最近要写入职培训教程。公司项目基本上都是基于Zend1框架,即使现在要转 Laravel 也肯定要有好长一段时间的过渡过程,而且基本上是新项目用 Laravel,老项目基本不会再重构了。因此,新人入职的话,还是需要培训一下 Zend Framework 1 的,之前把Zend官方文档的提供的一个入门教程翻译整理了一遍,作为入门教程,但这次又看了一遍之后发现,那篇教程只是教你如何做,是什么,却没有讲关于整个框架的整体的逻辑,所以,这篇文章就是为了解决这个问题的。阅读完本文之后,你将加深理解Zend1框架的启动、运行的完整流程。只有理解了这个完整的流程,才能在使用时或遇到问题时迅速解决问题,找到解决方案。

PS:我在梳理的时候,才发现其实我本身对于它也是不够了解的,业务上基本上熟悉了常用的东西之后,就不怎么关注框架本身的东西了,所以说这次整理也算是温故而知新,帮助别人的同时,也帮助了自己。

First of all

首先,我们从官网上下载Zend1的最新版本的 ZendFramework-1.12.20 源码包,然后解压,其目录结构简化如下:

ZendFramework-1.12.20
|-- bin
|   |-- zf.sh
|   `-- ...
|-- library
|   |-- Zend
`-- ...

现在我们只需关注两个:zf.sh 文件和 Zend 目录。zf.sh 是 Zend1 提供的一个命令行工具,用于创建 Project、Controller、Model 等类。接下来我将使用它来创建一个示例项目,为了更方便地全局使用该命令,把它链接到系统的环境变量PATH里面,执行命令如下:

$ ln -s /home/<user>/Downloads/ZendFramework-1.12.20/bin/zh.sh /usr/bin/zf

然后,可以用 zf 命令创建项目了:

$ zf create-project training

默认创建的项目目录结构如下:

training
|-- application
|   |-- Bootstrap.php
|   |-- configs
|   |   `-- application.ini
|   |-- controllers
|   |   |-- ErrorController.php
|   |   `-- IndexController.php
|   |-- models
|   `-- views
|       |-- helpers
|       `-- scripts
|           |-- error
|           |   `-- error.phtml
|           `-- index
|               `-- index.phtml
|-- library
|-- public
|   |-- .htaccess
|   `-- index.php
`-- tests
    |-- application
    |   `-- bootstrap.php
    |-- library
    |   `-- bootstrap.php
    `-- phpunit.xml

先浏览一下这个项目的目录结构,后面将会逐一分析每个文件和目录的作用。

index.php

因为所有的 Web 请求都将被重定位到 index.php上,所以先来看 index.php 的内容,:

<?php

// Define path to application directory
defined('APPLICATION_PATH')
    || define('APPLICATION_PATH', realpath(dirname(__FILE__) . '/../application'));

// Define application environment
defined('APPLICATION_ENV')
    || define('APPLICATION_ENV', (getenv('APPLICATION_ENV') ? getenv('APPLICATION_ENV') : 'production'));

// Ensure library/ is on include_path
set_include_path(implode(PATH_SEPARATOR, array(
    realpath(APPLICATION_PATH . '/../library'),
    get_include_path(),
)));

/** Zend_Application */
require_once 'Zend/Application.php';

// Create application, bootstrap, and run
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);
$application->bootstrap()
            ->run();

第4行和第8行定义了两个常量,APPLICATION_PATH - 应用的根路径,APPLICATION_ENV - 应用的运行环境,这两个常量是默认生成的,用于在配置文件(configs/application.ini)中指定相应路径。

第12行增加了PHP的include_path,默认PHP的include_path是在php.ini中指定的,这里把我们自己的library目录包括了进去,这样PHP在解析类的时候会到这个目录中去找,后面将会详述这个寻找类的过程。

第18行 require_once 'Zend/Application.php'; 包括了一个Application.php文件。PHP执行到这一步的时候,会去include_path列表里面寻找有没有Zend目录,然后再去Zend目录寻找有没有Application.php。我们继续执行,然后报了一个错误:

PHP Fatal error:  require_once(): Failed opening required 'Zend/Application.php' (include_path='/home/feiffy/Repo/feiffy/Training/library:.:/usr/share/php')

显然,PHP没有找到这个文件,所以报错了,它去了这三个目录(/home/feiffy/Repo/feiffy/Training/library,.,/usr/share/php)中去找了,都没有找到。这个文件是框架提供的,用于初始化Zend Application,之前我们只是用zf命令创建了基于Zend1的项目,但是没有把Zend1框架本身引入进去,所以报了这个错误。现在可以看到,PHP确实去我们设置的 /home/feiffy/Repo/feiffy/Training/library 去找了,所以可以把 Zend 框架放到这里,这里的 Zend 框架就是之前下载的 ZendFramework1.12.20/library/Zend 目录,将其整体的复制到 /home/feiffy/Repo/feiffy/Training/library 目录中即可。还有一种方式是直接建立软链接(相当于 Windows 中的快捷方式),我偏向于这种,这样减少了文件的复制:

$ ln -s /home/<user>/Downloads/ZendFramework-1.12.20/library/Zend /home/feiffy/Repo/feiffy/Training/library/Zend

这次PHP就能找到文件了,再说一遍其过程:PHP搜索include_path中的所有路径,发现在 /home/feiffy/Repo/feiffy/Training/library 中是存在 Zend/Application.php 文件的,所以就加载了它。

再看 Application.php 的内容:

<?php

class Zend_Application {
 ....
}

它定义了一个 Zend_Application 类,这个类就是整个 Zend 应用。

然后看第21行,实例化了一个 Zend_Application 应用,现在主要看传入的第二个参数:application.ini 的内容,现在全部是默认生成的:

[production]
...
includePaths.library = APPLICATION_PATH "/../library"
bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"
resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
...

这里只列出了一些重要的配置,这里的配置将会在后面的 Zend_Application 的初始化中会用到。

Application.php

现在我们来看 Zend_Application 的实例化过程。

<?php
class Zend_Application
{
    ...
    
    public function __construct($environment, $options = null, $suppressNotFoundWarnings = null)
    {
        require_once 'Zend/Loader/Autoloader.php';
        $this->_autoloader = Zend_Loader_Autoloader::getInstance();
        ...
        $options = $this->_loadConfig($options);
        $this->setOptions($options);
    }
}

其实就做了两件事:初始化 _autoloader 属性和 options 属性。

_autoloader 是 Zend1 框架自己实现的一个类加载器,其类名为 Zend_Loader_Autoloader,稍后,用到它的时候再讲它的加载类的过程,此处就把它当做应用的一个属性就好了。

然后,从配置文件 application.ini 转换为配置为一个 $options 数组,其内容如下:

<?php
$options = array(
    "includePaths" => "/home/feiffy/Repo/feiffy/Training/../library",
    "bootstrap" => array(
        "path" => "/home/feiffy/Repo/feiffy/Training/Bootstrap.php",
        "class" => "Bootstrap",
    ),
    "resources" => array(
        "frontController" => array(
            "controllerDirectory" => "/home/feiffy/Repo/feiffy/Training/controllers",
        ),
        ...
    ),
);

setOptions()

最后是 setOptions() 方法。

setOptions() 方法不仅设置了 _options 属性,还做了其他的初始化操作,主要的就是实例化了 _bootstrap 属性:

<?php
...
public function setOptions()
{
    $this->_options = $options; # 设置 _options 属性

    $this->setIncludePaths($options['includepaths']); # 设置include_paths,把ini里面的路径加到了原先的include_paths列表里面去

    # 初始化 _bootstrap 属性,后面会详述这个 _bootstrap 属性
    $bootstrap = $options['bootstrap'];
    $path  = $bootstrap['path'];
    $class = $bootstrap['class'];
    $this->setBootstrap($path, $class);
}

Bootstrap.php

setBootstrap()

setBootstrap() 设置应用的 _bootstrap 属性。

<?php
public function setBootstrap($path, $class)
{
    ...
    if (null == $class) {
        $class = 'Bootstrap';
    }
    require_once $path;
    $this->_bootstrap = new $class($this);
    ...
}

这段代码初始化了应用的 _bootstrap 属性,此时 $path 的值为:”/home/feiffy/Repo/feiffy/Training/application/Bootstrap.php”,文件内容如下:

<?php

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{


}

然后 $bootstrap = new $class(); 就是让PHP去找 Bootstrap 类,而在上一步中 require 了 Bootstrap.php 文件,所以最终在 /home/feiffy/Repo/feiffy/Training/application/Bootstrap.php 文件中找到了,于是实例化该类。

默认这个文件的内容是空的,但是它非常重要,应用所有的资源的初始化都要写在这个类里面。实例化 Bootstrap 时此处没有定义 __construct() 方法,所以PHP会去执行父类 Zend_Application_Bootstrap_Bootstrap 的 __construct() 方法。父类定义如下,可见它又继承了一个抽象父类。

class Zend_Application_Bootstrap_Bootstrap
    extends Zend_Application_Bootstrap_BootstrapAbstract
{
    parent::__construct($application);
}

bootstrap的主要功能都是由这个抽象父类提供的:

abstract class Zend_Application_Bootstrap_BootstrapAbstract
    implements Zend_Application_Bootstrap_Bootstrapper,
               Zend_Application_Bootstrap_ResourceBootstrapper
{
    public function __construct($application)
    {
        $this->setApplication($application);
        $options = $application->getOptions();
        $this->setOptions($options);
    }
}

Loader.php

但是这里有一个问题:Bootstrap 类所继承的 Zend_Application_Bootstrap_Bootstrap 类是如何找到它所在的类定义的文件的呢?这里并不像实例化Bootstrap类之前require了一个Bootstrap.php文件,到目前为止,所有require的文件中都没有包含Zend_Application_Bootstrap_Bootstrap 类的定义。上文在介绍 Bootstrap 的实例化时直接就跳转到了 Zend_Application_Bootstrap_Bootstrap->__construct() 方法,这中间必定经过了一个很重要的过程。这个过程就是PHP自动加载的过程,还记得之前提到的 Zend_Loader 类吗?在实例化 Zend_Application 类的时候添加了一个 _autoloader 属性。我们再回到上面详细看一下,它是如何被初始化的:

#1 Zend_Application
    require_once 'Zend/Loader/Autoloader.php';
    $this->_autoloader = Zend_Loader_Autoloader::getInstance();

#2 Zend_Loader_Autoloader
    public static function getInstance()
    {
        if (null === self::$_instance) {
            self::$_instance = new self();
        }
        return self::$_instance;
    }

    protected function __construct()
    {
        spl_autoload_register(array(__CLASS__, 'autoload'));
        $this->_internalAutoloader = array($this, '_autoload');
    }

第2行,Autoloader.php 中定义了 Zend_Loader_Autoloader 类,被 require 了,所以第3行能够加载该类并调用一个静态方法 getInstance(),其实就是实例化本身,实例化会自动调用__construct(),所以再去看它的 __construct() 方法。看到一个:**spl_autoload_register()**,这是什么?这是PHP用来注册自动加载函数的一个方法。这里就把 Zend_Loader_Autoloader->autoload() 方法注册为一个类自动加载器。当遇到需要解析类名的时候,就会自动找到这个类加载器,把类名交给它,然后它通过自己定义的规则,解析出类所在的文件名,加载该文件,然后就能实例化所需要的类了。

有了类加载器之后,上面在 require Bootstrap.php 文件时,发现 Bootstrap 类继承自 Zend_Application_Bootstrap_Bootstrap 类,然后 PHP 会去解析该类,结果发现现在所有的 require 的文件里面都没有该类的定义,默认的解析规则找不到类文件,所以就交给 Zend_Loader_Autoloader->autoload(),在 Zend_Loader_Autoloader 里面经过一番规则转换:

    public static function autoload($class)
    {
        call_user_func($autoloader, $class) // $autoloader->autoload()
    }

    protected function _autoload($class)
    {
        $callback = $this->getDefaultAutoloader();
        call_user_func($callback, $class); // $this->loadClass()
    }

    public static function loadClass($class, $dirs = null)
    {
        $file = self::standardiseFile($class);
        ...
        self::loadFile($file, $dirs, true);
    }

    public static function standardiseFile($file)
    {
        $fileName = ltrim($file, '\\');
        $file      = '';
        $namespace = '';
        if ($lastNsPos = strripos($fileName, '\\')) {
            $namespace = substr($fileName, 0, $lastNsPos);
            $fileName = substr($fileName, $lastNsPos + 1);
            $file      = str_replace('\\', DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
        }
        $file .= str_replace('_', DIRECTORY_SEPARATOR, $fileName) . '.php';
        return $file;    
    }

    public static function loadFile($filename, $dirs = null, $once = false)
    {
        if ($once) {
            include_once $filename;
        } else {
            include $filename;
        }
    }

经过层层调用,最终在 standardiseFile() 方法中,把 Zend_Application_Bootstrap_Bootstrap 转换为 “Zend/Application/Bootstrap/Bootstrap.php” 路径,然后在 loadFile() 方法中加载了该文件。加载文件是按照路径层级一级一级往下找时,PHP首先去 include_paths 目录列表中去寻找有没有 Zend 目录,结果发现在 /home/feiffy/Repo/feiffy/training/library 中找到了,并且后面的子目录也正确找到了,于是加载了 Bootstrap.php 文件,这个文件中定义了 Zend_Application_Bootstrap_Bootstrap 类。所以 PHP 现在知道了这个类在这个文件里,直接实例化它,并调用了 __construct()。

Zend1框架这种风格的加载文件的方式是老版PHP代码流行的风格,通过下划线来匹配目录层级,现在已经过时,这里只要了解一下就好了,新版的PHP自动加载风格请参考官方文档:PSR-4。

index.php

_bootstrap 属性实例化完成之后,就回到 index.php 中的 $application->bootstrap()->run();。应用的启动是通过 Bootstrap 类的 bootstrap() 方法启动的,调用顺序如下:

# index.php
$application->bootstrap()

# Zend_Application->bootstrap()
    public function bootstrap($resource = null)
    {
        $this->getBootstrap()->bootstrap($resource);
        return $this;
    }

# Zend_Application_Bootstrap_BootstrapAbstract->bootstrap()
    final public function bootstrap($resource = null)
    {
        $this->_bootstrap($resource);
        return $this;
    }

    protected function _bootstrap($resource = null)
    {
        if (null === $resource) {
            foreach ($this->getClassResourceNames() as $resource) {
                $this->_executeResource($resource);
            }

            foreach ($this->getPluginResourceNames() as $resource) {
                $this->_executeResource($resource);
            }
        } elseif (is_string($resource)) {
            $this->_executeResource($resource);
        } elseif (is_array($resource)) {
            foreach ($resource as $r) {
                $this->_executeResource($r);
            }
        } else {
            throw new Zend_Application_Bootstrap_Exception('Invalid argument passed to ' . __METHOD__);
        }
    }

最终在 _bootstrap() 中加载了所有的资源,至此应用的初始化、启动结束。接下来执行 run() 方法获取前端控制器资源,通过前端控制器处理路由、分发请求和输出响应。

# Zend_Application->run()
    public function run()
    {
        $this->getBootstrap()->run();
    }

# Zend_Application_Bootstrap_Bootstrap->run()
    public function run()
    {
        $front   = $this->getResource('FrontController');
        $default = $front->getDefaultModule();
        if (null === $front->getControllerDirectory($default)) {
            throw new Zend_Application_Bootstrap_Exception(
                'No default controller directory registered with front controller'
            );
        }

        $front->setParam('bootstrap', $this);
        $response = $front->dispatch();
        if ($front->returnResponse()) {
            return $response;
        }
    }

在这一步里面,初始化front前端控制器,由前端控制器负责把请求分发给相应的具体的Controller,返回Controller所产生的响应数据。到这一步之后就是后面,核心的类就是 Controller 类了。

Front.php

Front.php 中定义了 Zend_Controller_Front 即前端控制器,用于把请求分发给相应的具体的控制器,并且接收响应,路由功能就由它控制的。它有一个核心方法 dispatch():

class Zend_Controller_Front
{
    public function dispatch(Zend_Controller_Request_Abstract $request = null, Zend_Controller_Response_Abstract $response = null)
    {
        require_once 'Zend/Controller/Request/Http.php';
        $request = new Zend_Controller_Request_Http();
        $this->setRequest($request);
        ...
        require_once 'Zend/Controller/Response/Http.php';
        $response = new Zend_Controller_Response_Http();
        $this->setResponse($response);
        ...
        $router = $this->getRouter();

        $dispatcher = $this->getDispatcher();

        ...
        $dispatcher->dispatch($this->_request, $this->_response);
      
        ...
        $this->_response->sendResponse();
    }

    public function getRouter()
    {
        if (null == $this->_router) {
            require_once 'Zend/Controller/Router/Rewrite.php';
            $this->setRouter(new Zend_Controller_Router_Rewrite());
        }

        return $this->_router;
    }
}

在初始化空的 Request 和 Response 对象,以及设置了 router 对象之后,然后获取 FrontController 的 dispatcher 对象,调用该对象的 dispatch() 方法。FrontController 相当于分发流程的容器,真正实现路由分发的是 dispatcher 对象,即 Zend_Controller_Dispatcher_Standard 类:

class Zend_Controller_Dispatcher_Standard extends Zend_Controller_Dispatcher_Abstract
{
    public function dispatch(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response)
    {
        ...
        $className = $this->getControllerClass($request);
        if (!$className) {
            $className = $this->getDefaultControllerClass($request);
        }
        $moduleClassName = $className;
        ...
        $className = $this->loadClass($className);


        ...
        $controller = new $moduleClassName($request, $this->getResponse(), $this->getParams());

        ...
        $action = $this->getActionMethod($request);


        ...
        $controller->dispatch($action);


        ...
        $content = ob_get_clean();
        $response->appendBody($content);
    }
}

从 request 中获取 indexController 类名,加载类文件。这个方法里面解析到的 indexController 文件名为:”/home/feiffy/Repo/feiffy/Training/application/controllers/IndexController.php” 然后加载之。

第16行,最终实例化了具体的 IndexController 类,这就从框架层到了我们的业务层。

第23行,执行到业务action。

第27行,获取输出缓存中的数据,并清理输出缓存,将其内容添加到 response 对象中。执行完这一切之后,返回上层调用,继续执行到 FrontController,调用 reponse 对象的 sendResponse() 方法,输出内容到浏览器。

下面简要讲一讲 Reponse 对象:

abstract class Zend_Controller_Response_Abstract
{
    public function sendResponse()
    {
        $this->sendHeaders();

        ...

        $this->outputBody();
    }

    public function outputBody()
    {
        $body = implode('', $this->_body);
        echo $body;
    }
}

sendHeaders() 输出HTTP报文的头部,这是HTTP协议规定的内容就不必多说,outputBody() 方法输出内容部分,其实很简单,就是把 Response 对象中的 _body 数组里面存储的字符串值全部连接起来输出就OK了。

echo 函数默认是输出到标准输出(对于纯PHP脚本的话,就是屏幕或者控制台),但这里是 Web 项目,浏览器发出请求首先到 Apache 服务器,然后根据 Apache 的配置调用了 PHP 来接收请求,处理请求,所以这里的PHP输出会返回给 Apache,然后 Apache 再原样返回给浏览器。

Action.php

所有的 Controller 都继承自 Zend_Controller_Action 类:

abstract class Zend_Controller_Action implements Zend_Controller_Action_Interface
{
    public function __construct(Zend_Controller_Request_Abstract $request, Zend_Controller_Response_Abstract $response, array $invokeArgs = array())
    {
        $this->setRequest($request)
             ->setResponse($response)
             ->_setInvokeArgs($invokeArgs);
        $this->_helper = new Zend_Controller_Action_HelperBroker($this);
        $this->init();
    }
    
    public function init()
    {
    }


    public function dispatch($action)
    {
        $this->preDispatch();
        ...
        $this->$action();
        ...
        $this->postDispatch();


        ...
        $this->_helper->notifyPostDispatch();
    }
}

该类实例化时会执行 init() 方法,默认是空的,所以我们可以在自己写的 Controller 里面重写 init() 方法来做些初始化的工作。

最终使用 dispatch() 来调用 action(),preDispatch() 是在调用 action() 前的准备工作,postDispatch() 是在调用 action() 的收尾工作,我们可以在子类(自己写的 Controller )中的这两个方法里面可以加上对请求参数、返回响应做一些处理,或者单纯记录日志等工作。 而 action() 则是执行具体业务操作的方法。

现在我们再仔细看一下最后一个 notifyPostDispatch() 方法,运行到这里时,Controller 其实已经执行完了,这个方法主要通知相关的 helper 类更新它们的状态,其中有一个重要的 helper:Zend_Controller_Action_Helper_ViewRenderer,用来渲染视图层的类:

#1 Zend_Controller_Action_HelperBroker
    public function notifyPostDispatch()
    {
        foreach (self::getStack() as $helper) {
            $helper->postDispatch();
        }
    }

#2 Zend_Controller_Action_Helper_ViewRenderer
    public function postDispatch()
    {
        if ($this->_shouldRender()) {
            $this->render();
        }
    }

    public function render($action = null, $name = null, $noController = null)
    {
        $this->setRender($action, $name, $noController);
        $path = $this->getViewScript();
        $this->renderScript($path, $name);
    }

    public function renderScript($script, $name = null)
    {
        ...
        $this->getResponse()->appendBody(
            $this->view->render($script),
            $name
        );
        ...
    }

第19行初始化需要的东西,第20行获取需要渲染的phtml脚本文件,第21行渲染该文件。

getViewScript() 默认把 application/views/scripts/ 当做视图脚本文件的根目录,然后按照 controller/action 的命名规则去寻找相应的.phtml视图脚本文件(.phtml文件其实就是php和html代码混合的文件,php可以直接读取。),比如 index/index 的请求将会去找 index/index.phtml 文件。当然,这个是默认的配置,你也可以在 action() 方法中使用方法指定某个视图文件,这就不提了。

rederScript() 方法调用视图对象view的render()方法渲染脚本文件,那么渲染是什么意思呢?看View对象的定义就知道了.

View.php

abstract class Zend_View_Abstract implements Zend_View_Interface
{
    public function render($name)
    {
        // find the script file name using the parent private method
        $this->_file = $this->_script($name);
        unset($name); // remove $name from local scope

        ob_start();
        $this->_run($this->_file);

        return $this->_filter(ob_get_clean()); // filter output
    }

    protected function _run()
    {
        ...
        include func_get_arg(0);
    }

    private function _filter($buffer)
    {
        ...
        return $buffer;
    }
}

其实View对象渲染的原理很简单,就是先开启输出缓冲区ob_start(),然后include了一个视图文件(.phtml),这个文件里面非PHP的代码会直接输出,PHP的部分用echo或printf这种输出函数输出内容,输出缓冲开启之后,所有输出的内容会全部存在缓冲区里面,然后调用ob_get_clean() 获取缓冲区内容字符串并清理缓冲区,然后返回所有的字符串给上层调用。最后所有字符串内容通过 Response 对象的 appendBody() 方法添加到其 _body 属性里面,最后通过 Response 对象的输出方法,返回给 Apache,然后PHP结束运行。