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框架是如何被啟動的?