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結束運行。

PS - 個人博客原文:Zend_Framework_1框架是如何被啟動的?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM