月度归档:2013年08月

Zend Framework 1.x MVC可被初始化的资源

zend_application_resource

除了Resource.php和ResourceAbstract.php,其它的都是可以在配置中进行配置让其进行初始化的资源。

Resource.php定义了Zend_Application_Resource_Resource接口,它是规范这类资源必须要实现的方法,Zend_Application_Resource_ResourceAbstract实现了Zend_Application_Resource_Resource接口,资源类只要继承这个抽象类即可,也可以实现接口(在抽象类中给出的实现不符合要求时,不过可以在资源类中利用覆盖的办法也可以实现)。
zend_application_resource_extends

分析下Zend_Application_Resource_ResourceAbstract类:

    public function __construct($options = null)
    {
        if (is_array($options)) {
            $this->setOptions($options);
        } else if ($options instanceof Zend_Config) {
            $this->setOptions($options->toArray());
        }
    }

构造函数就是把配置传递给setOptions()方法。这个方法也没有什么特别:

    public function setOptions(array $options)
    {
        if (array_key_exists('bootstrap', $options)) {
            $this->setBootstrap($options['bootstrap']);
            unset($options['bootstrap']);
        }

        foreach ($options as $key => $value) {
            if (in_array(strtolower($key), $this->_skipOptions)) {
                continue;
            }

            $method = 'set' . strtolower($key);
            if (method_exists($this, $method)) {
                $this->$method($value);
            }
        }

        $this->_options = $this->mergeOptions($this->_options, $options);

        return $this;
    }

如果配置中指定了bootstrap,那么就会调用setBootstrap(),这个方法是接口规定需要实现的方法,所以一定是存在的。接下来就是如果配置项对应有set的方法,就调用它(这个很关键,很多资源实现都对应了set方法),最后是合并配置到_options中。

这个抽象了你最关键的就是上面段代码。

在Bootstrap对象中,资源的实例化实际是在它的_loadPluginResource()方法中进行的:

    protected function _loadPluginResource($resource, $options)
    {
        $options   = (array) $options;
        $options['bootstrap'] = $this;
        $className = $this->getPluginLoader()->load(strtolower($resource), false);

        if (!$className) {
            return false;
        }

        $instance = new $className($options);

        unset($this->_pluginResources[$resource]);

        if (isset($instance->_explicitType)) {
            $resource = $instance->_explicitType;
        }
        $resource = strtolower($resource);
        $this->_pluginResources[$resource] = $instance;

        return $resource;
    }

从这个方法可以看到,’bootstrap’对象是强制被赋值了当前的Bootrap对象,传递给资源类构造函数的,必定至少有bootstrap这个下标,并且它对应Bootrap的引用。在把资源文件装载进来后,就实例化这个资源。这个方法返回后,代码中还执行了实例的init()方法(init方法是接口规定要实现的,最终是具体类实现,抽象类中没有实现这个方法)。

以下分析Zend_Application_Resource_Db类,它在配置中的内容为:

resources.db.adapter = "PDO_MYSQL"
resources.db.params.host = "localhost"
resources.db.params.username = "root"
resources.db.params.password = "root"
resources.db.params.dbname = "zend_vfeelit"
resources.db.params.charset = "utf8"
resources.db.isDefaultTableAdapter = TURE
resources.db.params.driver_options.1002 = "SET NAMES UTF8"

这个db自然是代表Db类,里面的adapter 和 params 和 isDefaultTableAdapter就是携带的参数,在Zend_Application_Resource_Db中对应了setAdapter 和 setParams 和 setIsDefaultTableAdapter方法,这些方法在对象构建时就会被调用(不明白这个机制就需要回头看对Zend_Application_Resource_ResourceAbstract类的分析),重点需要看init()方法的实现:

    public function init()
    {
        if (null !== ($db = $this->getDbAdapter())) {
            if ($this->isDefaultTableAdapter()) {
                Zend_Db_Table::setDefaultAdapter($db);
            }
            return $db;
        }
}

// getDbAdapter
    public function getDbAdapter()
    {
        if ((null === $this->_db)
            && (null !== ($adapter = $this->getAdapter()))
        ) {
            //echo $adapter; // PDO_MYSQL
            $this->_db = Zend_Db::factory($adapter, $this->getParams());
        }
        return $this->_db;
}

getDbAdapter()内部调用getAdapter(),getAdapter()返回适配器字符串,这里就是”PDO_MYSQL”,如果没有指定,那么就是空,一路返回就什么都没有执行。否则就调用了Zend_Db的工厂方法生成了一个适配器对象,保存到了$this->_db中,如果在应用中想回去这个适配器对象,可以用:

$application->getBootstrap()->getPluginResource(“db”)

以下分析Zend_Application_Resource_Frontcontroller类,它在配置中的内容为:

resources.frontController.controllerDirectory = APPLICATION_PATH "/controllers"
resources.frontController.params.displayExceptions = 0

这个类文件实际很简单,就是前段控制器的初始化:

class Zend_Application_Resource_Frontcontroller extends Zend_Application_Resource_ResourceAbstract
{
    /**
     * @var Zend_Controller_Front
     */
    protected $_front;

    /**
     * Initialize Front Controller
     *
     * @return Zend_Controller_Front
     */
    public function init()
    {
        $front = $this->getFrontController();

        foreach ($this->getOptions() as $key => $value) {
            switch (strtolower($key)) {
                case 'controllerdirectory':
                    if (is_string($value)) {
                        $front->setControllerDirectory($value);
                    } elseif (is_array($value)) {
                        foreach ($value as $module => $directory) {
                            $front->addControllerDirectory($directory, $module);
                        }
                    }
                    break;

                case 'modulecontrollerdirectoryname':
                case 'moduledirectory':
                case 'defaultcontrollername':
                case 'defaultaction':
                case 'defaultmodule':
                case 'baseurl':
                case 'params':
                case 'plugins':
                case 'returnresponse':
                case 'throwexceptions':
                case 'actionhelperpaths':
                case 'dispatcher':
                default:
                    $front->setParam($key, $value);
                    break;
            }
        }
		//setBootstrap()方法在对象构建时被调用,this->_bootstrap记录了Bootrap的引用,这里还反过来填充Bootstrap中的frontController,不一定有必要
        if (null !== ($bootstrap = $this->getBootstrap())) {
            $this->getBootstrap()->frontController = $front;
        }

        return $front;
    }

    public function getFrontController()
    {
        if (null === $this->_front) {
            $this->_front = Zend_Controller_Front::getInstance();
        }
        return $this->_front;
    }
}

其它的资源类基本如此,封装或包装其它的组件类。

不过这里需要特别点出来的是,init()方法通常都会把自己实例化的对象返回,比如Zend_Application_Resource_Frontcontroller类就是负责初始化前端控制器Zend_Controller_Front类的,所以它的init()方法也会返回一个Zend_Controller_Front类对象。在插件资源被初始化时(在Bootstrap类的_executeResource方法中),有一段是实例化插件资源的关键代码:

        if ($this->hasPluginResource($resource)) {
//             echo $resource."<br />";
//             frontcontroller
//             db
//             layout
            $this->_started[$resourceName] = true;
//             echo "--->";
            $plugin = $this->getPluginResource($resource);
//             echo "<----";
            $return = $plugin->init();
            unset($this->_started[$resourceName]);
            $this->_markRun($resourceName);

            if (null !== $return) {
                $this->getContainer()->{$resourceName} = $return;
            }

            return;
        }

注意看,init()方法返回值被$return接收,然后把它注入一个叫容器的对象中(可以看做是一个全局数组),以后要取回这个对象,只要getResource(“资源名”)即可。比如FrontControler对象就使用$this->_container->FrontControler来记录这个引用。明白这个是很重要的,因为$this->_container是一个Zend_Registry类对象,它继承了ArrayObject,ArrayObject是SPL内置对象,意思就是说可以像操作对象一样操作数组,end_Registry类对实现单态模式,全局只有一个实例,任何时候都可以通过Zend_Registry::getInstance()获取实例引用,从而实现全局设计模式,实际可以把它看做是一个全局数组,不管是在控制器还是在视同中,都可以轻易地获取这个全局数组中的资源,比如数据库适配器,前端控制器等。

永久链接:http://blog.ifeeline.com/985.html

Zend Framework 1.x 插件装载器的实现

在Bootstrap对象的bootstrap()方法初始化资源时,资源的类文件是通过插件装载器加载的。

在Zend Framework 1.x的MVC实现中,框架的资源是被看做成插件,然后使用Zend_Loader_PluginLoader对象来进行装载。

在Bootstrap类的setOptions()方法中,有如下代码:

        if (array_key_exists('pluginpaths', $options)) {
            //返回一个Zend_Loader_PluginLoader
            $pluginLoader = $this->getPluginLoader();  

            foreach ($options['pluginpaths'] as $prefix => $path) {
                $pluginLoader->addPrefixPath($prefix, $path);
            }
            unset($options['pluginpaths']);
        }

这个告诉我们,可以在配置中添加pluginpaths添加插件路径,比如:

pluginpaths.Vfeelit = “Test/Com/Vfeelit”
pluginpaths.Ifeelit = “Com/Ifeelit”

注意,这个getPluginLoader()方法,有默认路径会被添加:

//Zend_Application_Bootstrap_Bootstrap
    public function getPluginLoader()
    {
        if ($this->_pluginLoader === null) {
            $options = array(
                'Zend_Application_Resource'  => 'Zend/Application/Resource',
                'ZendX_Application_Resource' => 'ZendX/Application/Resource'
            );

            $this->_pluginLoader = new Zend_Loader_PluginLoader($options);
        }
        print_r($this->_pluginLoader);//******
        return $this->_pluginLoader;
    }
 
//print_r($this->_pluginLoader);
Zend_Loader_PluginLoader Object
(
    [_loadedPluginPaths:protected] => Array
        ()

    [_loadedPlugins:protected] => Array
        ()
    [_prefixToPaths:protected] => Array
        (
            [Zend_Application_Resource_] => Array
                (
                    [0] => Zend/Application/Resource/
                )
            [ZendX_Application_Resource_] => Array
                (
                    [0] => ZendX/Application/Resource/
                )
            [Vfeelit_] => Array
                (
                    [0] => Test/Vfeelit/
                )
            [Ifeeline_] => Array
                (
                    [0] => Test/Ifeeline/
                )
        )
    [_useStaticRegistry:protected] => 
)

注意看这个名字prefixToPaths,硬生生翻译就是前缀对应的路径,实际实现的也是这个意思。

接下来分析Zend_Loader_PluginLoader的构建过程:

//构造函数
    public function __construct(Array $prefixToPaths = array(), $staticRegistryName = null)
    {
        //array("Com_Vfeelit"=>"Com/Vfeelit")  第二部分给出路径,可以不跟前缀一一对应
        foreach ($prefixToPaths as $prefix => $path) {
            $this->addPrefixPath($prefix, $path);
        }
    }

调用addPrefixPath()函数添加类前缀和对应的路径前缀。

    public function addPrefixPath($prefix, $path)
    {
        //这里限制了 一个前缀 只能对应一个路径, 因为路径只能是字符串
        if (!is_string($prefix) || !is_string($path)) {
	      //异常
        }

        $prefix = $this->_formatPrefix($prefix);
        //  去掉正反斜杆
        $path   = rtrim($path, '/\\') . '/';  

        if ($this->_useStaticRegistry) {
            self::$_staticPrefixToPaths[$this->_useStaticRegistry][$prefix][] = $path;
        } else {
            if (!isset($this->_prefixToPaths[$prefix])) {
                $this->_prefixToPaths[$prefix] = array();
            }
            //一个前缀可以对应多个路径
            if (!in_array($path, $this->_prefixToPaths[$prefix])) {
                $this->_prefixToPaths[$prefix][] = $path;
            }
        }
        return $this;
    }

这个addPrefixPath()方法把类前缀和路径添加到$this->_prefixToPaths数组中,这个数组结构:

//         Array
//         (
//         		[Vfeelit_] => Array
//         		(
//         				[0] => Test/Com/Vfeelit/
//         				[1] => Com/Vfeelit/
//         		)
        
//         		[Ifeeline_] => Array
//         		(
//         				[0] => Test/Com/Ifeeline/
//         		)  

注意观察,一个类前缀可以有多个路径,不过无法通过传递参数给构造函数实现,因为构造函数实际把路径部分传递给addPrefixPath()方法,而它限制路径参数必须是字符串,否则抛出异常,换句话说就是:

new Zend_Loader_PluginLoader(array(“Vfeelit”=>”Com/Vfeelit”));//合法
new Zend_Loader_PluginLoader(array(“Vfeelit”=>array(“Com/Vfeelit”,”Test/Com/Vfeelit”))); //非法

##可以通过如下方式实现
$loader = Zend_Loader_PluginLoader();
$loader->addPrefixPath(“Vfeelit”,”Test/Com/Vfeelit”);
$loader->addPrefixPath(“Vfeelit”,”Com/Vfeelit”);
//         Array
//         (
//         		[Vfeelit_] => Array
//         		(
//         				[0] => Test/Com/Vfeelit/
//         				[1] => Com/Vfeelit/
//         		)
//		   )

对象的构建基本就是注册类前缀和路径前缀。实际的类文件的加载,需要使用load()方法:

    public function load($name, $throwExceptions = true)
    {
        //比如name是db,经过_formatName()后得到Db
        $name = $this->_formatName($name);
        //是否已经加载过,$_loadedPlugins保存已经加载的插件,避免多次加载
        if ($this->isLoaded($name)) {
            //$this->_loadedPlugins[$name]的值是类名
            return $this->getClassName($name);
        }

        if ($this->_useStaticRegistry) {
            $registry = self::$_staticPrefixToPaths[$this->_useStaticRegistry];
        } else {
            //array("Com_Vfeelit"=>"Com/Vfeelit")
            $registry = $this->_prefixToPaths;
        }
//翻转路径数组
//         print_r($registry);
//         Array
//         (
//         		[Vfeelit_] => Array
//         		(
//         				[0] => Test/Com/Vfeelit/
//         				[1] => Com/Vfeelit/
//         		)
        
//         		[Ifeeline_] => Array
//         		(
//         				[0] => Test/Com/Ifeeline/
//         		)  
//         )
        $registry  = array_reverse($registry, true);
//         print_r($registry);
//         Array
//         (
//         		[Ifeeline_] => Array
//          		(
//          				[0] => Test/Com/Ifeeline/
//         		 )
        
//         		[Vfeelit_] => Array
//         		(
//         				[0] => Test/Com/Vfeelit/
//         				[1] => Com/Vfeelit/
//         		)
        
//         )
//         从结构上看,一个前缀可以对应多个路径,不过不能通过构造函数的方式传递,需要构造了对象之后调用addPrefixPath
        $found     = false;
        if (false !== strpos($name, '\\')) {
            $classFile = str_replace('\\', DIRECTORY_SEPARATOR, $name) . '.php';
        } else {
            $classFile = str_replace('_', DIRECTORY_SEPARATOR, $name) . '.php';
        }
        //echo $classFile; //Db.php
        $incFile   = self::getIncludeFileCache();
        foreach ($registry as $prefix => $paths) {
            $className = $prefix . $name;
//             echo $className;   ->   Vfeelit_Db 
            if (class_exists($className, false)) {
                $found = true;
                break;
            }
          
            //一个前缀可以对应多个路径  寻找类文件,最后添加的路径最先应用
            $paths     = array_reverse($paths, true);

            foreach ($paths as $path) {
                $loadFile = $path . $classFile;
//                 echo $loadFile."**";  Vfeelit/Db.php**
                if (Zend_Loader::isReadable($loadFile)) { //可读
                    include_once $loadFile;
                    if (class_exists($className, false)) {
                        if (null !== $incFile) {
                            self::_appendIncFile($loadFile);
                        }
                        $found = true;
                        break 2;
                    }
                }
            }
        }
	//找不到,抛出异常
        if (!$found) {
        }

        if ($this->_useStaticRegistry) {
            self::$_staticLoadedPlugins[$this->_useStaticRegistry][$name]     = $className;
        } else {
	    //把类名记录下来
            $this->_loadedPlugins[$name]     = $className;
        }
        return $className;
    }

这个load()方法的过程,实际不算复杂。举例说明:

    public function indexAction()
    {
        $loader = new Zend_Loader_PluginLoader();
	$loader->addPrefixPath("Vfeelit","Test/Com/Vfeelit");
	$loader->addPrefixPath("Vfeelit","Com/Vfeelit");
		
	print_r($loader);
		
	$className = $loader->load("Db");
	if($className){
		$db = new $className();
	}
    }
//输出--------->
Zend_Loader_PluginLoader Object
(
    [_loadedPluginPaths:protected] => Array
        (
        )

    [_loadedPlugins:protected] => Array
        (
        )

    [_prefixToPaths:protected] => Array
        (
            [Vfeelit_] => Array
                (
                    [0] => Test/Com/Vfeelit/
                    [1] => Com/Vfeelit/
                )

        )

    [_useStaticRegistry:protected] => 
)
/usr/local/httpd-2.2.27/htdocs/zf/library/Com/Vfeelit/Db.php

首先组合成路径Com/Vfeelit/Db.php和Test/Com/Vfeelit/Db.php,然后分别加载文件,每次加载后就检查Vfeelit_Db类是否已经存在,如有存在后面的检查马上终止,这个意味着需要在Com/Vfeelit/Db.php或Test/Com/Vfeelit/Db.php文件中定义Vfeelit_Db类。如果两个文件中都定义了这个类,那么最后添加的路径会优先使用(一旦匹配,就终止循环)。***一定要看明白这段文字才能理解插件装载器。它跟自动装载器的区别在于,插件装载器的类名不需要很路径一一对应,而自动装载器的类文件需要跟类名严格对应,比如Zend_Config_Xml对应Zend/Config/Xml.php文件,而对应使用插件装载器,它的路径理论可以任意指定,比如指定前缀Vfeelit对应了路径Test/Com/Vfeelit/,那么Vfeelit开头的类就去这个路径寻找对应的类。

下面是一个实际例子:

//application/application.ini添加
pluginpaths.Vfeelit = "Test/Com/Vfeelit"
pluginpaths.Ifeeline = "Com/Vfeelit"

//在library下(只要能通过include_path搜索到即可)添加Test/Com/Vfeelit和Com/Vfeelit
//并分别添加Db.php文件,内容:
class Vfeelit_Db{
	public function __construct(){
		echo __FILE__;
	}
	public function out(){
		echo "Plugin Test";
	}
}

//application/Bootstrap.php添加方法
class Bootstrap extends Zend_Application_Bootstrap_Bootstrap
{
	public function _initPluin(){
		$pluginLoader = $this->getPluginLoader();
		
		$registry = Zend_Registry::getInstance();		
		$registry->set('plugin_loader', $pluginLoader);
	}

}

//在某个控制器方法中添加如下代码
    public function indexAction()
    {
		$registry = Zend_Registry::getInstance();
		$pluginLoader = $registry->get("plugin_loader");
		$className = $pluginLoader->load("db");
		
		$db = new $className();
		
		$db->out();
    }

//输出-------------------------
/usr/local/httpd-2.2.27/htdocs/zf/library/Com/Vfeelit/Db.phpPlugin Test

总体套路是把插件装载器放入注册表中,在具体控制器方法中通过这个注册表获取这个插件装载器,然后调用它的load()方法把具体插件的类文件加载进来,然后生成对应的插件类实例,调用其方法。

这个所谓的插件,玛尼我还有手动加载类文件,也算奇葩了。

通过Bootstrap类提供的自动执行_init开头的方法,把需要使用到的资源放入注册表,然后在其它地方通过注册表获取资源,是这类兼容PHP 5.2.X的框架的通用做法。

永久链接:http://blog.ifeeline.com/981.html

Zend Framework 1.x Bootstrap对象的bootstrap()方法实现资源初始化

在研究完Bootstrap对象的构建之后,就需要研究一下bootstrap()方法是如何实现资源初始化操作的:

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

这是一个final方法,意味着不能覆盖,内部实际调用_bootstrap()方法:

    protected function _bootstrap($resource = null)
    {
       
        if (null === $resource) {
            foreach ($this->getClassResourceNames() as $resource) {
                $this->_executeResource($resource);
            }
/*
    public function getClassResourceNames()
    {
        $resources = $this->getClassResources();
        return array_keys($resources);
    }

    //继续调用getClassResources()
    //把_init开头的方法放入$this->_classResources
    public function getClassResources()
    { 
        if (null === $this->_classResources) {
            if (version_compare(PHP_VERSION, '5.2.6') === -1) {
                $class        = new ReflectionObject($this);
                $classMethods = $class->getMethods();
                $methodNames  = array();

                foreach ($classMethods as $method) {
                    $methodNames[] = $method->getName();
                }
            } else {
                $methodNames = get_class_methods($this);
            }

            $this->_classResources = array();
            foreach ($methodNames as $method) {
                if (5 < strlen($method) && '_init' === substr($method, 0, 5)) {
                    $this->_classResources[strtolower(substr($method, 5))] = $method;
                }
            }
        }
        //Array ( [view] => _initView ) 
        return $this->_classResources;
    }
*/
            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()传递一个资源进入进行初始化(一般是指Zend/Application/Resource下的插件,要么就是Bootstrap类的某个init方法)。默认没有指定要初始化的资源,那么就是指初始化配置中的指定所有资源,这个所有的资源获取是通过getClassResourceNames()和getPluginResourceNames()获取的(注意,这里的ClassResource玛尼就是指Bootstrap类的init方法)。

仔细看看getClassResources()方法,它把自身对象的所有以”_init”开头的方法,压入$this->_classResources数组。比如_initView()方法,那么就会得到Array ( [view] => _initView )。这样,就可以把某些初始化工作放入到自定义的bootstrap类的以_init开头的方法中。

回到_bootstrap()方法内部,$this->getClassResourceNames()就是获取了以_init开头的方法名,然后通过_executeResource()执行,它下面的代码也非常类似,不过它针对的是记录到$this->_pluginResources数组中的内容(这个数组在Bootstrap的构造函数中会填充,配置中如果没有设置resources,则没有内容,但是在Bootstrap的构造函数中会判断如果FrontController没有注册,则把它注册进来,所以_pluginResources至少会有FrontController)。

接下来重点就是_executeResource()方法细节:

    protected function _executeResource($resource)
    {
        $resourceName = strtolower($resource);

        if (in_array($resourceName, $this->_run)) {
            return;
        }
	//每个资源的代码如何正在执行中,直接返回,可以避免执行多次,也是代码互斥
        if (isset($this->_started[$resourceName]) && $this->_started[$resourceName]) {
            throw new Zend_Application_Bootstrap_Exception('Circular resource dependency detected');
        }
	//针对类资源,就是bootstrap类总的_init开头的方法
        $classResources = $this->getClassResources();
        if (array_key_exists($resourceName, $classResources)) {
	    //开始互斥
            $this->_started[$resourceName] = true; 	
            $method = $classResources[$resourceName];
            $return = $this->$method(); //执行方法
	    //解除互斥
            unset($this->_started[$resourceName]);
	    //标志已经运行
            $this->_markRun($resourceName);		
	    //如果有返回内容,那么把内容记录到内容容器中
            if (null !== $return) {
                $this->getContainer()->{$resourceName} = $return;
            }

            return;
        }
	//资源如果已经存在,跟以上逻辑基本一致,
        //不过这里的插件资源是调用它本身的init()方法初始化的
        if ($this->hasPluginResource($resource)) {
            $this->_started[$resourceName] = true;
            $plugin = $this->getPluginResource($resource);
            $return = $plugin->init(); //****
            unset($this->_started[$resourceName]);
            $this->_markRun($resourceName);
            //还可以接收返回值
            if (null !== $return) {
                $this->getContainer()->{$resourceName} = $return;
            }

            return;
        }

        throw new Zend_Application_Bootstrap_Exception('Resource matching "' . $resource . '" not found');
    }

$this->getPluginResource($resource);负责把对应资源的类读取进来,然后生成一个实例返回(内部调用插件装载器的load()方法加载类文件,如果已经是实例则直接返回)。得到实例后才能调用它的init()方法对资源进行初始化。为了知道其中的细节,继续跟踪getPluginResource()方法:

    public function getPluginResource($resource)
    {
        //print_r($this->_pluginResources);
        
        if (array_key_exists(strtolower($resource), $this->_pluginResources)) {
            $resource = strtolower($resource);
            //资源不是Zend_Application_Resource_Resource类型(默认是字符串或数组)
            if (!$this->_pluginResources[$resource] instanceof Zend_Application_Resource_Resource) {
                //$this->_loadPluginResource()用资源对应的配置生成一个实例,
                //这个实例替换$this->_pluginResources数组资源对应的值,返回资源名
                $resourceName = $this->_loadPluginResource($resource, $this->_pluginResources[$resource]);
                if (!$resourceName) {
                    throw new Zend_Application_Bootstrap_Exception(sprintf('Unable to resolve plugin "%s"; no corresponding plugin with that name', $resource));
                }

                $resource = $resourceName;
            }
            //这个时候返回一个实例
            return $this->_pluginResources[$resource];
        }

        //传递进来的资源名称 不在 $this->_pluginResources中时
        foreach ($this->_pluginResources as $plugin => $spec) {
            if ($spec instanceof Zend_Application_Resource_Resource) {
                $pluginName = $this->_resolvePluginResourceName($spec);
                if (0 === strcasecmp($resource, $pluginName)) {
                    unset($this->_pluginResources[$plugin]);
                    $this->_pluginResources[$pluginName] = $spec;
                    return $spec;
                }
                continue;
            }

            if (false !== $pluginName = $this->_loadPluginResource($plugin, $spec)) {
                if (0 === strcasecmp($resource, $pluginName)) {
                    return $this->_pluginResources[$pluginName];
                }
                continue;
            }
	    // 比如 class Vfeelit extends Zend_Application_Resource_Resource{}, 这里的Vfeelit类如果先被加载了,那么它就可用
            // 换句话说,只要是继承了Zend_Application_Resource_Resource,并且类已经加载,都可以作为资源进行配置
            if (class_exists($plugin)
            && is_subclass_of($plugin, 'Zend_Application_Resource_Resource')
            ) { //@SEE ZF-7550
                $spec = (array) $spec;
                $spec['bootstrap'] = $this;
                $instance = new $plugin($spec);
                $pluginName = $this->_resolvePluginResourceName($instance);
                unset($this->_pluginResources[$plugin]);
                $this->_pluginResources[$pluginName] = $instance;

                if (0 === strcasecmp($resource, $pluginName)) {
                    return $instance;
                }
            }
        }

        return null;
    }

如果指定的资源在$this->_pluginResources中,但是对应的值不是对象(默认只是配置值),那么就调用_loadPluginResource($resource, $options)方法,很显然,它是专门负责初始化资源的:

    protected function _loadPluginResource($resource, $options)
    {
        $options   = (array) $options;
        $options['bootstrap'] = $this;
        $className = $this->getPluginLoader()->load(strtolower($resource), false);

        if (!$className) {
            return false;
        }

        $instance = new $className($options);

        unset($this->_pluginResources[$resource]);

        if (isset($instance->_explicitType)) {
            $resource = $instance->_explicitType;
        }
        $resource = strtolower($resource);
        $this->_pluginResources[$resource] = $instance;

        return $resource;
    }

这个方法内部实际调用了$this->getPluginLoader()方法获取了一个插件装载器(可以查看插件装载器实现),它调用这个装载器的load()方法把资源装载进来,然后生成一个实例,然后更新$this->_pluginResources[$resource](现在记录的一个对象)。

在getPluginResource()函数中比较不好理解的是后部分,实际上,在Bootstrap的构造函数中,调用了$this->hasPluginResource(‘FrontController’)方法,这个方法传入了FrontController字符串,这个字符串最终进入getPluginResource()函数后转换成frontcontroller,拿个key来进行比对,但实际记录的是fontController,所以无法匹配,所以它会执行getPluginResource()函数的后半部分,在如下这部分代码(换句话说就是大小写不一致导致的特殊的处理,靠啊,全部装换成小写比对不可以??):

            if (false !== $pluginName = $this->_loadPluginResource($plugin, $spec)) {
                if (0 === strcasecmp($resource, $pluginName)) {
                    return $this->_pluginResources[$pluginName];
                }
                continue;
            }

它使用了strcasecmp()函数进行了不区分大小写匹配,这样就能正确工作了。

大概总结一下这个资源初始化做了什么工作:
1 执行在自定义bootstrap类中定义的以_init开头的方法
2 把$_pluginResources中的插件实例化(利用插件装载器加载对应的类文件,生成实例,对应更新$_pluginResources数组,比如db对应的一个配置数组,经过bootstrap()之后,db对应的已经是一个用之前的配置数组构建的Zend_Application_Resource_Db类型的对象,并且已经执行了它的init()方法)
3 注意这里的资源都是配置中用resource字段开头定义的资源,这些资源必须存在于Zend/Application/Resource中(或者是实现了Zend_Application_Resource_ResourceAbstract的插件),比如Frontcontroller,Db,Layout,Router,Session,View等,是ZF中MVC构成的一部(注意,这些文件命名务必是只有第一个字母大写,否则load文件时会产生问题,所有插件都应该遵守这个原则,因为插件类名是组装产生的)。
4 如果配置中没有指定需要初始化的资源,那么Frontcontroller也会初始化,否则就无法进行下面的路由分发。

理清插件资源的装载初始化过程,资源从bootstrap()方法开始,内部调用_bootstrap()方法,具体执行资源还是使用了_executeResource()方法,这个方法使用getPluginResource()方法获取资源实例,如何获取的就是这个方法负责的内容了,实际它调用_loadPluginResource()方法进行资源实例化,然后更新$this->_pluginResources[$resource] = $instance(原来记录的是资源的配置参数,现在记录的是对应的对象)。实际上,_bootstrap()和_executeResource()方法的实现都可以放入bootstrap()方法中,后面的getPluginResource()方法也可以不使用_loadPluginResource()方法,不过这样划分有利用代码重构与代码重用。

简单来说,就是先按照顺序执行Bootstrap的_init开头的方法,然后分别实例化插件(在配置文件中指定),实例化时是把配置信息传入构造函数,然后调用实例的init方法,实例化后的方法保存在_pluginResources数组中,可以通过getPluginResource()获取实例引用。在Bootstrap对象中,classResource实际就是指Bootstrap中_init开头的方法,pluginResource就是指Zend/Application/Resource下的资源(或是继承了Zend_Application_Resource_ResourceAbstract的插件),resource是指模块内的资源,比如Model类。

——————————————-
–最佳实践
如果要使自定义插件自动初始化,自定义的插件类要继承Zend_Application_Resource_ResourceAbstract,然后要在配置文件中用pluginpaths指定自定义插件前缀和对应搜索路径,接着是在配置中用resources配置这个自定义插件的参数。举例:

//application/application.ini
pluginpaths.Vfeelit = "Test/Com/Vfeelit"
resources.test = ''

//往Test/Com/Vfeelit添加Test.php
class Vfeelit_Test extends Zend_Application_Resource_ResourceAbstract{
	public function init(){
		echo __FILE__;
	}
}

//输出
/usr/local/httpd-2.2.27/htdocs/zf/library/Test/Com/Vfeelit/Test.php

这里就是通过配置使自定义插件可用的方法。也可以自定义跟Zend/Application/Resource中的插件同名的方法,这样可以覆盖ZF的实现。如果只是执行一段代码,写一个_init开头的方法即可,不用费周折实现一个插件,这个也是常用做法。

永久链接:http://blog.ifeeline.com/979.html

Zend Framework 1.x Bootstrap对象构建

在入口文件index.php中会初始化一个叫Zend_Appliction的对象,这个对象主要是把配置读入,做一些初始化,接下来入口文件index.php的末尾有:

//index.php
$application->bootstrap()
            ->run();

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

    public function getBootstrap()
    {
        if (null === $this->_bootstrap) {
            $this->_bootstrap = new Zend_Application_Bootstrap_Bootstrap($this);
        }
        return $this->_bootstrap;
    }

可见,Application的bootstrap()方法内部是通过调用getBootstrap()方法获取Bootstrap对象的,获取这个对象后就直接调用这个对象的bootstrap()方法,控制权就交给了bootstrap对象。

注意到Application的getBootstrap()方法是在没有设置$this->_bootstrap的情况下才实例化一个Zend_Application_Bootstrap_Bootstrap对象,不过在ZF中,配置文件中通常配置了自定义的bootstrap:

bootstrap.path = APPLICATION_PATH "/Bootstrap.php"
bootstrap.class = "Bootstrap"

这两个值在Application的构造函数中调用setOptions()方法时被使用(实际调用setBootstrap()方法设置生成一个对象赋值给$this->_bootstrap,这个对象是继承了Zend_Application_Bootstrap_Bootstrap的自定义对象)。在配置中设置的这个bootstrap位于application/的Bootstrap.php中,这个类默认只是继承Zend_Application_Bootstrap_Bootstrap:

class Bootstrap extends Zend_Application_Bootstrap_Bootstrap{}

可以在这个类内添加任意的_init开头的方法用来初始化自定义资源,至于如何实现的,就是下面要讨论的内容。

Zend_Application_Bootstrap_Bootstrap 继承了 Zend_Application_Bootstrap_BootstrapAbstract,而Zend_Application_Bootstrap_BootstrapAbstract实现了Zend_Application_Bootstrap_Bootstrapper和Zend_Application_Bootstrap_ResourceBootstrapper接口,Zend_Application_Bootstrap_BootstrapAbstract的构造函数:

//Zend_Application_Bootstrap_BootstrapAbstract
    public function __construct($application)
    {
        $this->setApplication($application);
        $options = $application->getOptions();
        $this->setOptions($options);
    }

把$application引用赋值给Bootstrap对象的_appliction,把来自$appliction的配置传递给Bootstrap对象的setOptions()方法:

//Zend_Application_Bootstrap_BootstrapAbstract
    public function setOptions(array $options)
    {
        $this->_options = $this->mergeOptions($this->_options, $options);

        $options = array_change_key_case($options, CASE_LOWER);
        $this->_optionKeys = array_merge($this->_optionKeys, array_keys($options));

        $methods = get_class_methods($this);
        foreach ($methods as $key => $method) {
            $methods[$key] = strtolower($method);
        }

        if (array_key_exists('pluginpaths', $options)) {
            //返回一个Zend_Loader_PluginLoader
            $pluginLoader = $this->getPluginLoader();  

            foreach ($options['pluginpaths'] as $prefix => $path) {
                $pluginLoader->addPrefixPath($prefix, $path);
            }
            unset($options['pluginpaths']);
        }

        foreach ($options as $key => $value) {
            $method = 'set' . strtolower($key);
            if (in_array($method, $methods)) {
                $this->$method($value);
            } elseif ('resources' == $key) {
                foreach ($value as $resource => $resourceOptions) {
                    //echo $resource."---".$resourceOptions."<br />"; frontController,db,layout
                    $this->registerPluginResource($resource, $resourceOptions);
                }
            }
        }
        return $this;
    }

这个方法会检查配置中是否设置了pluginpaths,如果有就初始化一个Zend_Loader_PluginLoader(这个内容的细节可参看Zend Framework 1.x 插件装载器的实现),接下来的这个循环就比较有意思了。它循环所有的配置,如果当前这个对象中存在对应的set方法,就直接调用它来设置,这个奇葩告诉我们,可以在自定义的Bootstrap类中添加和配置名同名的set方法,用它来实现自定义设置。配置文件中一般都要设置appnamespace,那么对应的setappnamespace方法就会被调用(注意是存在对应方法时方法才会被调用)。这样,appnamespace对应的值就被设置到了_appNamespace中。如果配置没有对应的set方法并且这个配置值叫resources,循环调用registerPluginResource()方法,这个方法只是把resource的资源进行注册(说直接点就是把配置信息放入_pluginResources中,比如db和实例化它时需要用到的配置信息):

//print_r($this->_pluginResources);
//         Array
//         (
//         		[frontController] => Array
//         		(
//         				[controllerDirectory] => D:\www\web\vfeelit\application/controllers
//         				[params] => Array
//         				(
//         						[displayExceptions] => 1
//         				)
        
//         		)
        
//         		[db] => Array
//         		(
//         				[adapter] => PDO_MYSQL
//         				[params] => Array
//         				(
//         						[host] => localhost
//         						[username] => root
//         						[password] => root
//         						[dbname] => zend_vfeelit
//         						[charset] => utf8
//         						[driver_options] => Array
//         						(
//         								[1002] => SET NAMES UTF8
//         						)
        
//         				)
        
//         				[isDefaultTableAdapter] => TURE
//         		)
        
//         		[layout] => Array
//         		(
//         				[layoutPath] => D:\www\web\vfeelit\application/layouts/scripts/
//         		)
        
//         )

实际上就是把resource读入$this->_pluginResources数组,这个数组的下标就是配置中的第二层次的值。这个需要说明的是:这里注册的插件(通过配置文件的resource指定的),需要跟Zend/Application/Resource目录下的资源对应,并且相关的配置信息也要按照套路来设置,因为在实例化这些资源时(插件),这些配置信息会按照预定义格式进行读取(这个细节在Zend Framework 1.x Bootstrap对象的bootstrap()方法实现资源初始化分析)。

以上是Zend_Application_Bootstrap_BootstrapAbstract构造函数完成的工作,Zend_Application_Bootstrap_Bootstrap继承了Zend_Application_Bootstrap_BootstrapAbstract,所以它提供了自己的构造函数:

    public function __construct($application)
    {
        parent::__construct($application);

        //如果配置中设置了resourceloader,就是指定了资源装载器
        if ($application->hasOption('resourceloader')) {
            $this->setOptions(array(
                'resourceloader' => $application->getOption('resourceloader')
            ));
        }       
//     	$this->_resourceLoader = Zend_Application_Module_Autoloader(array(
//         'namespace' => $namespace,
//         'basePath'  => dirname($path),
//     	))
        $this->getResourceLoader();

        if (!$this->hasPluginResource('FrontController')) {
            $this->registerPluginResource('FrontController');
        }
    }

这个构造函数中,首先调用了父类的构造函数,这就是为何要先分析父类构造函数的原因。接下来调用getResourceLoader()方法,实际得到一个Zend_Application_Module_Autoloader()对象赋值给$this->_resourceLoader(用它来加载模块资源,比如models的加载)。最后查看FrontController是否已经注册到插件资源中,没有就注册到插件资源中(配置中默认都有指定,如果不指定,这里就产生一个,它是前端控制器,关系到后面的路由分发,所以必须设置)。

这里还是要提及一下getResourceLoader()方法:

    public function getResourceLoader()
    {
       
        //配置文件中有设置appnamespace,这个是在bootstrap对象的构造函数中调用setOptions()方法时,
        //setAppNamespace()方法被调用
        if ((null === $this->_resourceLoader)
            && (false !== ($namespace = $this->getAppNamespace()))
        ) {
            $r    = new ReflectionClass($this);
            $path = $r->getFileName();
            $this->setResourceLoader(new Zend_Application_Module_Autoloader(array(
                'namespace' => $namespace,
                'basePath'  => dirname($path),
            )));
        }
        return $this->_resourceLoader;
    }

这段代码的getAppNamespace()返回$this->_appnamesapce,这个值是在bootstrap对象的构造函数中调用setOptions()方法时设置的(上面已经分析),所以这里会返回Zend_Application_Module_Autoloader(array(‘namespace’ => ‘application’,’basePath’ => ‘/path/to/application’)),这个所谓的资源装载器主要完成比如模型的装载,这个后面继续分析。

Bootstrap对象的构造函数就这些内容,总结一下:
$this->_resourceLoader = Zend_Application_Module_Autoloader()
$this->_pluginResources = 配置节点resource的第二层内容

接下来就是调用Bootstrap的bootstrap()方法。

永久链接:http://blog.ifeeline.com/977.html

Zend Framework 1.x 框架的配置设置详解

Zend_Application的构造函数:

    public function __construct($environment, $options = null)
    {
        $this->_environment = (string) $environment;

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

        if (null !== $options) {
            if (is_string($options)) {
                $options = $this->_loadConfig($options);
            } elseif ($options instanceof Zend_Config) {
                $options = $options->toArray();
            } elseif (!is_array($options)) {
                throw new Zend_Application_Exception('Invalid options provided; must be location of config file, a config object, or an array');
            }
	    $this->setOptions($options);
	}
    }

传递进来的$options可以是字符串,可以是Zend_Config类型的对象,也可以是数组。如果是字符串,则使用_loadConfig()方法把字符串的文件中的配置加载进来。如果继续跟踪这个方法:

    protected function _loadConfig($file)
    {
        $environment = $this->getEnvironment();
        $suffix      = pathinfo($file, PATHINFO_EXTENSION);
        $suffix      = ($suffix === 'dist')
                     ? pathinfo(basename($file, ".$suffix"), PATHINFO_EXTENSION)
                     : $suffix;

        switch (strtolower($suffix)) {
            case 'ini':
                $config = new Zend_Config_Ini($file, $environment);
                break;

            case 'xml':
                $config = new Zend_Config_Xml($file, $environment);
                break;

            case 'json':
                $config = new Zend_Config_Json($file, $environment);
                break;

            case 'yaml':
            case 'yml':
                $config = new Zend_Config_Yaml($file, $environment);
                break;

            case 'php':
            case 'inc':
		/////
	)
    }

可以发现,这个配置文件的类型(字符串后缀判断),可以是ini,xml,json,yaml(yml)或者php(inc),如果是php那么就直接返回数组,如果不是根据后缀对应使用一个类对象进行处理,最后调用toArray()返回数组。注意每个类对象实例化时都传递了$environment进入,这个是控制获取哪部分参数的代码。

Zend_Application构造函数中获取配置参数后最后调用setOptions()方法。这个方法负责设置参数的规范:

    public function setOptions(array $options)
    {
	//可以传递一个叫config的参数,值可以是单个值,也可以是数组,如果是数组则循环调用_loadConfig()把配置合并进来
	//比如includePaths.library = APPLICATION_PATH "/../library",就会变成$options['includePaths']=array('library'=>'')
	//这个config可以是config.abc=123;config.def=456 就会变成$options['config']=array('abc'=>'','def'=>'')
	//也可以config.abc=tst.ini;config.def=xl.xml 指定文件路径
        if (!empty($options['config'])) {
            if (is_array($options['config'])) {
                $_options = array();
                foreach ($options['config'] as $tmp) {
                    $_options = $this->mergeOptions($_options, $this->_loadConfig($tmp));
                }
                $options = $this->mergeOptions($_options, $options);
            } else {
                $options = $this->mergeOptions($this->_loadConfig($options['config']), $options);
            }
        }

        $this->_options = $options;

        $options = array_change_key_case($options, CASE_LOWER);

        $this->_optionKeys = array_keys($options);

	//接下来有5个配置项是需要特别对待的
        if (!empty($options['phpsettings'])) {
//             		[phpSettings] => Array
//             		(
//             				[display_startup_errors] => 1
//             				[display_errors] => 1
//             		)
	    //直接调用ini_set()设置,比如init_set('display_errors',1),此为配置PHP环境
            $this->setPhpSettings($options['phpsettings']);
        }

        if (!empty($options['includepaths'])) {
//             		[includePaths] => Array
//             		(
//             				[library] => D:\www\web\vfeelit\application/../library
//             		)
	    //使用set_include_path()设置include_path,$options['includepaths']是数组,可以是多个,默认这里把library添加
	    //实际上index.php中已经设置了一次
            $this->setIncludePaths($options['includepaths']);
        }

        if (!empty($options['autoloadernamespaces'])) {
	    //实际调用自动装载器的registerNamespace()方法注册名空间,自动转载器不会去装载没有注册的名空间的类(默认只有Zend_和ZendX_,如果希望使用ZF自动装载第三方类库,必须注册命空间,注意区别注册名空间和给名空间指定装载器,不同概念)
            $this->setAutoloaderNamespaces($options['autoloadernamespaces']);
        }

        if (!empty($options['autoloaderzfpath'])) {
            $autoloader = $this->getAutoloader();
            if (method_exists($autoloader, 'setZfPath')) {
                $zfPath    = $options['autoloaderzfpath'];
                $zfVersion = !empty($options['autoloaderzfversion'])
                           ? $options['autoloaderzfversion']
                           : 'latest';
                $autoloader->setZfPath($zfPath, $zfVersion);
            }
        }

        if (!empty($options['bootstrap'])) {
//             		[bootstrap] => Array
//             		(
//             				[path] => D:\www\web\vfeelit\application/Bootstrap.php
//             				[class] => Bootstrap
//             		)
	    //从配置文件获取并设置bootstrap,最终生成一个bootstrap对象赋值给$this->_bootstrap
            $bootstrap = $options['bootstrap'];

            if (is_string($bootstrap)) {
                $this->setBootstrap($bootstrap);
            } elseif (is_array($bootstrap)) {
                if (empty($bootstrap['path'])) {
                    throw new Zend_Application_Exception('No bootstrap path provided');
                }

                $path  = $bootstrap['path'];
                $class = null;

                if (!empty($bootstrap['class'])) {
                    $class = $bootstrap['class'];
                }
		//实例化bootstrap对象,会把$application引用传递进入
                $this->setBootstrap($path, $class);
            } else {
                throw new Zend_Application_Exception('Invalid bootstrap information provided');
            }
        }

        return $this;
    }

Zend_Application对象已经构建起来,这个对象用$this->_options记录所有传递进来的配置值,$this->_optionKeys记录了所有配置值的下标,$this->_bootstrap指向了Bootstrap对象。

之后运行$application的bootstrap()方法,这个bootstrap()获取$this->_bootstrap对象,调用它的bootstrap(),最后$application的bootstrap()方法返回了$application的引用,接着继续调用run()方法,实际也是调用$this->_bootstrap对象的run()方法。

这个bootstrap()就是资源初始化,run()就是初始化完成后开始运行(获取前端控制器,调用dispatch()方法进入路由分发)。

接下来理清楚一下Zend_Config组件的关系:
Zend_Config负责把配置参数装入对象,这是所有类型配置对象的父类,可以把一个数组传入Zend_Config获取一把最一般的配置对象,如果要把其它类型的配置装入,可以使用它的子类,比如Zend_Config_Ini、Zend_Config_Json、Zend_Config_Xml,继承关系:
Zend_Config组件

Zend_Config_Ini默认要求传递一个ini文件字符串给它,它负责把文件的配置读入,第二参数指定要读入的段,INI文件中可以设置多个段。

可以把读入的参数保存为另一种形式,过程是,把类型为Zend_Config的对象通过Zend_Config_Writer写入文件,至于要写到哪种类型文件,还要依赖它的子类,比如Zend_Config_Writer_Ini、Zend_Config_Writer_Xml、Zend_Config_Writer_Json、Zend_Config_Writer_Array(写入文件,类型是数组配置方式)

zend_config_writer

$configArray = new Zend_Config_Writer_Array();

$configArray->setConfig(new Zend_Config_Ini(APPLICATION_PATH . '/configs/application.ini'));
$configArray->write("D:/conf.php");

###输出文件的内容
<?php
return array (
  'production' => 
  array (
    'phpSettings' => 
    array (
      'display_startup_errors' => '0',
      'display_errors' => '0',
    ),
    'includePaths' => 
    array (
      'library' => 'D:\\www\\web\\vfeelit\\application/../library',
    ),
    'bootstrap' => 
    array (
      'path' => 'D:\\www\\web\\vfeelit\\application/Bootstrap.php',
      'class' => 'Bootstrap',
    ),
    'appnamespace' => 'Application',
    'resources' => 
    array (
      'frontController' => 
      array (
        'controllerDirectory' => 'D:\\www\\web\\vfeelit\\application/controllers',
        'params' => 
        array (
          'displayExceptions' => '0',
        ),
      ),
      'db' => 
      array (
        'adapter' => 'PDO_MYSQL',
        'params' => 
        array (
          'host' => 'localhost',
          'username' => 'root',
          'password' => 'root',
          'dbname' => 'zend_vfeelit',
          'charset' => 'utf8',
          'driver_options' => 
          array (
            1002 => 'SET NAMES UTF8',
          ),
        ),
        'isDefaultTableAdapter' => 'TURE',
      ),
      'layout' => 
      array (
        'layoutPath' => 'D:\\www\\web\\vfeelit\\application/layouts/scripts/',
      ),
    ),
  ),
  'staging' => array (),
  'testing' => array (),
  'development' => array (),
);

这样把Ini文件转换成了Array配置文件。

永久链接:http://blog.ifeeline.com/973.html

Zen-cart 插件 – 蛋疼的Fast and Easy AJAX Checkout for Zen Cart

之前曾经写过一篇文件时关于这个插件的,链接为http://blog.ifeeline.com/260.html,随着鬼佬的这个版本的更新,这篇文章中的Bug已经得到修改,不过恶心的鬼佬在版本升级中又引入了恶心的Bug,其中的一个Bug简直让人非常恶心。随我慢慢道来…

注:这里使用的quick_checkout版本是2.25.2

Zen-cart插件Bug
如果希望修改地址时弹出,或者像把这里的Change Address Pop-up改为true,不幸的是,这个弹出框需要一个第三方的JS插件支持,而这个插件是收费的。如果把它改为了true,而没有安装这个JS插件,将产生脚本错误。

插件对tpl_modules_checkout_new_address.php做了修改,不过很脑残,这个鬼佬的程序非常不严谨:

<?php
if (ACCOUNT_STATE == 'true') {
 	if ($flag_show_pulldown_states == true) {
		?>
		<label class="inputLabel" for="stateZone" id="zoneLabel"><?php echo ENTRY_STATE. (zen_not_null(ENTRY_STATE_TEXT) ? '&nbsp;<span class="alert">' . ENTRY_STATE_TEXT . '</span>' : ''); ?></label>
		<?php
   		echo zen_draw_pull_down_menu('zone_id', zen_prepare_country_zones_pull_down($selected_country), $zone_id, 'id="stateZone"'); 
    }
	?>

	<?php 
	if ($flag_show_pulldown_states == true) { ?>
		<br class="clearBoth" id="stBreak" />
	<?php 
	} ?>
	<label class="inputLabel" for="state" id="stateLabel"><?php echo $state_field_label; ?></label>
	<?php
    echo zen_draw_input_field('state', '', zen_set_field_length(TABLE_ADDRESS_BOOK, 'entry_state', '40') . ' id="state"');
	if (zen_not_null(ENTRY_STATE_TEXT)) echo '&nbsp;<span class="alert" id="stText">' . ENTRY_STATE_TEXT . '</span>';
    if ($flag_show_pulldown_states == false) {
      	echo zen_draw_hidden_field('zone_id', $zone_name, ' ');
    }
	?>
	<br class="clearBoth" />
<?php
}
?>

这段代码本没有问题,图示:

后台开启State,并且State总是下拉时:
Zen-cart打开省份下拉

当一个国家没有分配省时,要求自己填写,注意看后面的那个星号。
Zen-cart国家省份下来

这是以上代码:

if (zen_not_null(ENTRY_STATE_TEXT)) echo '&nbsp;<span class="alert" id="stText">' . ENTRY_STATE_TEXT . '</span>';

产生的作用。不过鬼佬的代码妈的的把这行去掉了,真是自作聪明啊,导致了切换省份时触发的JS函数错误:

  function hideStateField(theForm) {
    theForm.state.disabled = true;
    theForm.state.className = 'hiddenField';
    theForm.state.setAttribute('className', 'hiddenField');
    document.getElementById("stateLabel").className = 'hiddenField';
    document.getElementById("stateLabel").setAttribute('className', 'hiddenField');
    document.getElementById("stText").className = 'hiddenField';
    document.getElementById("stText").setAttribute('className', 'hiddenField');
    document.getElementById("stBreak").className = 'hiddenField';
    document.getElementById("stBreak").setAttribute('className', 'hiddenField');
  }

document.getElementById(“stText”)这行代码,由于stText不存在,导致JS脚本错误。

接下来的这个大Bug简直让人恶心:
点击访问http://www.vfeelit.com/index.php?main_page=checkout_shipping_address,输入新地址,那么应该应用新地址,新地址添加由模块checkout_new_address.php(被覆盖),这个逻辑没有问题,$_SESSION[‘sendto’] = $new_id;说明被设置了最新添加的地址,然后它会被定位到quick_check,打开header_php.php看:

        $check_address = $db->Execute("SELECT customers_default_shipping_address_id FROM " . TABLE_CUSTOMERS . " WHERE customers_id = " . (int)$_SESSION['customer_id'] . " AND customers_default_shipping_address_id > 0 LIMIT 1;"); 
        // set the default shipping address id if it exists (we need to do this in case the customer logged in from outside of FEAC)
        if ($check_address->RecordCount() > 0) $_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id'];

看那行注释,说是当客户不使用FEAC登录进来时,需要设置。注意,这里的customers_default_shipping_address_id是FEAC插件的扩展。它用来记录默认运输地址。这里的意思就是当设置了这个ID时,$_SESSION[‘sendto’]永远都使用这个。

你会看到改变地址,也会定向到quick_checkout,也会设置$_SESSION[‘sendto’],那么上面的代码岂不是覆盖掉这个设置? 是的,TMD你会发现你添加了新地址,新地址成功添加,但是它不给你应用(它应用了所谓的默认运输地址,这个鬼佬真他妈脑残)。

实际上,你看到,当客户不是使用FEAC登录时,$_SESSION[‘sendto’]没有被设置,而$_SESSION[‘customer_default_shipping_address_id’]也没有被设置,所以这里的这个代码是顾此失彼。可以判断如果没有设置,才运行这个代码:

        if ($check_address->RecordCount() > 0){
			if((int)$_SESSION['sendto'] > 0){ //已经被赋值,通过FEAX登录
				$_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id']; //实际通过FEC登录时这个已经被设置
			}else{
				$_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_address->fields['customers_default_shipping_address_id']; //非FEAX登录
			}
		}

在注册新用户时,customers_default_shipping_address_id并没设置(或设置为0),注意,$_SESSION[‘customer_default_shipping_address_id’]这个customer没有s。当从地址簿中选择地址簿时,选中的这个地址会被设置为customers_default_shipping_address_id:

//checkout_new_address.php(模块)
				// update default shipping address ID
				if (!$_SESSION['COWOA']) {
					$db->Execute("UPDATE " . TABLE_CUSTOMERS . " 
						  SET customers_default_shipping_address_id = " . (int)$_POST['address'] . " 
						  WHERE customers_id = " . (int)$_SESSION['customer_id'] . " LIMIT 1;");
				}

实际上,当添加新的运输地址时,也应该设置为默认的运输地址。

在qc_login的header_php.php中:

$_SESSION['sendto'] = $_SESSION['customer_default_shipping_address_id'] = $check_customer->fields['customers_default_shipping_address_id'];

登录时$_SESSION[‘sendto’]就被设置为默认的运输地址。在quick_checkout包含的文件quick_checkout_fec.php中有对这些地址的修正:

  if (!$_SESSION['sendto']) {
    $_SESSION['sendto'] = $_SESSION['customer_default_address_id'];
  } else {
  // verify the selected shipping address
    $check_address_query = "SELECT count(address_book_id) AS total
                            FROM   " . TABLE_ADDRESS_BOOK . "
                            WHERE  customers_id = :customersID
                            AND    address_book_id = :addressBookID
                            LIMIT 1";

    $check_address_query = $db->bindVars($check_address_query, ':customersID', (int)$_SESSION['customer_id'], 'integer');
    $check_address_query = $db->bindVars($check_address_query, ':addressBookID', (int)$_SESSION['sendto'], 'integer');
    $check_address = $db->Execute($check_address_query);

    if ($check_address->fields['total'] != '1') {
      $_SESSION['sendto'] = $_SESSION['customer_default_address_id'];
      $_SESSION['shipping'] = '';
    }
  }

意思是先应用默认运输地址,然后才应用默认地址。

这里描述的这个Bug是非常恶心的。可能是,这个插件前后经过不同的人维护,顾此失彼导致严重Bug,以前75美金的插件,现在卖150美金,安装还收费,其它依赖的也是收费,想钱想疯了。

还有一个删除地址簿的Bug,也是顾此失彼,不过问题不大就不罗列了。附加文件保留:quick_checkout_BUG_Fix

永久链接:http://blog.ifeeline.com/962.html

Zen-cart 改变运输地址 和 改变账单地址 流程 和 小Bug

改变运输地址 和 改变账单地址

main_page=checkout_shipping_address   			
main_page=checkout_payment_address

这种情况都会加载地址薄表单 和 地址簿列表:
Zen-cart修改地址流程
这个流程展示文件是如何包含的。

tpl_modules_checkout_address_book.php模板中,对应默认地址的选中:

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $_SESSION['sendto']), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

单选框是否被选中,完全依赖当前的$_SESSION[‘sendto’],从上面的示例图可以看到tpl_modules_checkout_address_book.php模板会被两个控制器(checkout_shipping_address 和 checkout_payment_address)包含,地址是否选中不应该只依赖当前的$_SESSION[‘sendto’],我发现在Zen-cart151的默认模板中都是如此设置的,这明显是一个Bug,不过问题不大。

首先在tpl_modules_checkout_address_book.php开头添加

if($addressType == 'billto'){
	$checked = $_SESSION['billto'];
}else{
	$checked = $_SESSION['sendto'];
}

然后把

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $_SESSION['sendto']), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

替换成:

<?php echo zen_draw_radio_field('address', $addresses->fields['address_book_id'], ($addresses->fields['address_book_id'] == $checked), 'id="name-' . $addresses->fields['address_book_id'] . '"'); ?>

这样的小Bug在Zen-cart目前所有版本都是如此,那意思是说只要点修改账单地址,点一下继续,那的账单地址就可以被改变了,而你的本意可能是希望不做任何修改。

永久链接:http://blog.ifeeline.com/959.html

UTF-8编码在PHP中的怪异问题

在ThinkPHP说明文档中 入门->命名规范中有这段话:
“另外有一点非常关键,ThinkPHP默认全部使用UTF-8编码,所以请确保你的程序文件采用UTF-8编码格式保存,并且去掉BOM信息头(去掉BOM头信息有很多方式,不同的编辑器都有设置方法,也可以用工具进行统一检测和处理),否则可能导致很多意想不到的问题。”

它是说用UTF-8编码保存文件需要把BOM信息头去掉。BOM信息头是个什么东西?可以参看:
关于字符编码的基本知识
Windows中的UTF-8与Unicode实验

实际上,UTF-8没有字节序的问题,或者说能自动识别,BOM就是文件开头的几个字节,用来标示文件的编码(大头还是小头),PHP中如果使用UTF-8编码保存文件,需要去掉BOM,否则出现诡异问题:
PHP UTF-8编码问题

这里的head标签是配对的,但是浏览器无法识别,看起来是它们的内部编码有问题,不过这种问题,让人百思不得其解,你一定无法想到这个问题是UTF-8编码携带了BOM导致,不管怎么样,我把包含的用UTF-8编码的文件的BOM去掉,这个诡异问题就得到解决了:
UTF-8编码去掉BOM

PHP中对Unicode编码的支持还是存在一些问题,在使用UTF-8编码时务必把BOM去掉。我花了很多时间跟踪代码才找到这个诡异问题的根源,此为一个经验教训,希望各位看官不要重复这样的过程,很悲剧。

永久链接:http://blog.ifeeline.com/952.html