标签归档:Zend Framework

Zend Framework 2.x 之 Zend\Db

zend-db

Driver是数据库驱动抽象,负责和具体的数据库建立链接,如果要获取这个链接,就需要通过Adapter的Driver。Platform是针对不同平台的抽象,比如不同平台用不同的字符把字段圈起来。

Adapter需要确定Driver 和 Platform,对不同驱动和平台,Adapter提供了一个通用接口,这个就是适配器设计模式了。

$adapter = new Zend\Db\Adapter\Adapter(array(
		'driver' => 'pdo',
		'dsn' => 'mysql:dbname=test;hostname=localhost',
		'username' => 'root',
		'password' => '',
		'driver_options' => array(
				PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
		),
));

$adapter = new Zend\Db\Adapter\Adapter(array(
    'hostname' => 'localhost',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    
    'driver' => 'mysqli',
    'charset' =>'utf8'
    
    //'driver' => 'pdo_mysql',
    //'driver_options' => array(
    //    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
    //),
));
//当不使用PDO时,字符集的指定使用charset,有时候我们有理由相信mysqli驱动会比PDO好,因为它抽象多了一层。注意:不管是否使用PDO,driver_options都可以使用,不过需要指定针对具体驱动的选项。

这是一段范本代码。看看手册中的说法:
“It is important to know that by using this style of adapter creation, the Adapter will attempt to create any dependencies that were not explicitly provided. A Driver object will be created from the configuration array provided in the constructor. A Platform object will be created based off the type of Driver class that was instantiated. And lastly, a default ResultSet object is created and utilized. Any of these objects can be injected, to do this, see the next section.”

如果使用这种风格创建适配器,适配器试图去创建所有的没有明确提供的依赖(依赖具体的驱动,平台)。一个Driver对象将根据适配器构造函数中提供的数组参数来创建。一个Platform对象将根据Driver被实例化时的类的类型来创建。最后,一个默认的ResultSet对象被创建并可用。所有这些对象都是可注入的。

use Zend\Db\Adapter\Platform\PlatformInterface;
use Zend\Db\ResultSet\ResultSet;

class Zend\Db\Adapter\Adapter {
    public function __construct($driver, PlatformInterface $platform = null, ResultSet $queryResultSetPrototype = null)
}

玛尼,意思就是你可以通过构造函数去指定具体的驱动,平台,结果集。(第一参数可以是配置数组 或 Driver对象)

适配器提供一个相对原始的query()方法,可以预处理SQL语句:

$result = $adapter->query('SELECT * FROM `site` WHERE `id` = ?', array(5));

内部工作流程:
1 创建一个新的Statement对象
2 如果需要,预处理一个数组到ParameterContainer
3 把ParameterContainer注入到Statement对象
4 执行Statement对象,处理结果对象
5 检查结果对象确定提供的sql是查询或or a result set producing statement
6 如果是结果的语句,克隆ResultSet的prototype,inject Result as datasource
7 否则返回Result

也可直接执行SQL语句,参考:

$r = $adapter->query('ALTER TABLE `site` ADD `test` int NULL', Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);

一般执行最多的是SELECT INSERT UPDATE DELETE语句,以下是一个范本:

$sttm = $adapter->query('SELECT * FROM `site`');
$resultSet = $sttm ->execute();
foreach($resultSet as $rr){
	print_r($rr);
}

最好可以去看看query()方法的实现:

    public function query($sql, $parametersOrQueryMode = self::QUERY_MODE_PREPARE)
    {
        if (is_string($parametersOrQueryMode) && in_array($parametersOrQueryMode, array(self::QUERY_MODE_PREPARE, self::QUERY_MODE_EXECUTE))) {
            $mode = $parametersOrQueryMode;
            $parameters = null;
        } elseif (is_array($parametersOrQueryMode)) {
            $mode = self::QUERY_MODE_PREPARE;
            $parameters = $parametersOrQueryMode;
        } else {
            throw new Exception\InvalidArgumentException('Parameter 2 to this method must be a flag, an array, or ParameterContainer');
        }

        if ($mode == self::QUERY_MODE_PREPARE) {
            $this->lastPreparedStatement = null;
            $this->lastPreparedStatement = $this->driver->createStatement($sql);
            $this->lastPreparedStatement->prepare();
            if (is_array($parameters) || $parameters instanceof ParameterContainer) {
                $this->lastPreparedStatement->setParameterContainer((is_array($parameters)) ? new ParameterContainer($parameters) : $parameters);
                $result = $this->lastPreparedStatement->execute();
            } else {
                return $this->lastPreparedStatement;
            }
        } else {
            $result = $this->driver->getConnection()->execute($sql);
        }

        if ($result instanceof Driver\ResultInterface && $result->isQueryResult()) {
            $resultSet = clone $this->queryResultSetPrototype;
            $resultSet->initialize($result);
            return $resultSet;
        }

        return $result;
    }

第二参数默认是预处理(self::QUERY_MODE_PREPARE)。首先如果是字符串(预处理 或 直接执行),参数就是null,如果是一个数组,那么就是预处理,参数就是这个数组。

如果是预处理,就调用驱动的createStatement($this->driver->createStatement($sql)),然后执行statement的prepare方法,如果有参数提供,就调用setParameterContainer方法,然后执行statement的execute方法,如果没有参数提供就直接把这个statement返回。

如果不是预处理,直接调用:$result = $this->driver->getConnection()->execute($sql);

最后判断是否是查询,是查询就返回结果集。否则就是一个结果对象。

总结,三种可能,statement对象,结果对象,结果集对象。首先根据是否是预处理,如果是但是没有绑定参数,直接返回statement对象,如果需要绑定参数,则把执行结果用结果对象保存起来,如果是执行语句,也把结果用结果对象保存,最后根据sql是否是查询,确定是返回结果集还是结果对象。

事实:如果返回结果集,必定是查询。对于查询,这里只是再次封装一遍而已。 实际不用再次封装也是可以使用的。以下是一些例子,对比返回的类型,可以更好理解:

// 不指定第二参数,为预处理模式,没有需要的参数绑定,直接返回Statement
// Zend\Db\Adapter\Driver\Pdo\Statement
$stmt = $adapter->query('SELECT * FROM `site`');
// Zend\Db\Adapter\Driver\Pdo\Result
$result= $stmt->execute();
foreach($result as $rr){
	//print_r($rr);
}

// 第二参数为一个数组,为预处理模式,绑定的参数为指定的数组,内部执行Statement的execute
// 返回结果或结果集
// Zend\Db\ResultSet\ResultSet
$resultSet = $adapter->query('SELECT * FROM `site` WHERE `id` > ?', array(1));
foreach($resultSet as $rr){
	//print_r($rr);
}

// 第二参数直接指定为执行模式,返回结果或结果集
// 可以看做和上面用法等同
// Zend\Db\ResultSet\ResultSet
$resultSet = $adapter->query('SELECT * FROM `site`', Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
foreach($resultSet as $rr){
	//print_r($rr);
}

// 直接执行模式,返回结果(一般INSERT UPDATE这些返回一个结果,查询返回结果集)
// Zend\Db\Adapter\Driver\Pdo\Result
$result = $adapter->query("INSERT INTO `site`(`platform_id`,`name`) values('2','中国')", Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
// 返回插入的ID
echo $result->getGeneratedValue();


// Zend\Db\Adapter\Driver\Pdo\Result
$result = $adapter->query("UPDATE `site` SET `name` = '中国' where id=12", Zend\Db\Adapter\Adapter::QUERY_MODE_EXECUTE);
// 更新受影响的行数
echo $result->getAffectedRows();

这里需要说明的是,getAffectedRows()是Zend\Db\Adapter\Driver\Pdo\Result的方法,返回的是受影响的行数,而Zend\Db\ResultSet\ResultSet并没有这个方法,Zend\Db\ResultSet\ResultSet的toArray()可以把结果集转换成一个数组输出。

Zend\Db\ResultSet\ResultSet是一个更抽象的东西,封装了Zend\Db\ResultSet\Result。

在理解了以上内容的前提下,我们可以不使用query(),而直接使用statement。

// Zend\Db\ResultSet\Result
$statement = $adapter->createStatement($sql, $optionalParameters);
$result = $statement->execute();

注意看上面的例子中的$adapter->createStatement(),这个方法内部实际调用$this->driver->createStatement(),driver中提供了三个对象:

Zend\Db\Adapter\Driver\ConnectionInterface
Zend\Db\Adapter\Driver\StatementInterface
Zend\Db\Adapter\Driver\ResultInterface

Statement 和 Result上面已经讨论过了(通过adapter中间接调用,adapter中的query()会调用到driver的execute)。以下是driver中必须知道的方法:

$cnn = $adapter->getDriver()->getConnection();
$cnn->beginTransaction(); 	//开始事务
$cnn->commit();			//提交事务
$cnn->rollback();		//回滚事务
$cnn->isConnected()		//是否已链接
$cnn->getLastGeneratedValue()   //最后插入的ID

适配器在自动构建SQL语句时,需要它的另一个对象支持,Zend\Db\Adapter\Platform。直接看例子:

$platform = $adapter->getPlatform();
// or
$platform = $adapter->platform; // magic property access


/** @var $adapter Zend\Db\Adapter\Adapter */
/** @var $platform Zend\Db\Adapter\Platform\Sql92 */
$platform = $adapter->getPlatform();

// "first_name"
echo $platform->quoteIdentifier('first_name');

// "
echo $platform->getQuoteIdentifierSymbol();

// "schema"."mytable"
echo $platform->quoteIdentifierChain(array('schema','mytable')));

// '
echo $platform->getQuoteValueSymbol();

// 'myvalue'
echo $platform->quoteValue('myvalue');

// 'value', 'Foo O\\'Bar'
echo $platform->quoteValueList(array('value',"Foo O'Bar")));

// .
echo $platform->getIdentifierSeparator();

// "foo" as "bar"
echo $platform->quoteIdentifierInFragment('foo as bar');

// additionally, with some safe words:
// ("foo"."bar" = "boo"."baz")
echo $platform->quoteIdentifierInFragment('(foo.bar = boo.baz)', array('(', ')', '='));

Zend\Db\Sql\Sql提供了比较强大的,面向对象的SQL书写方法。

use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select(); // @return Zend\Db\Sql\Select
$insert = $sql->insert(); // @return Zend\Db\Sql\Insert
$update = $sql->update(); // @return Zend\Db\Sql\Update
$delete = $sql->delete(); // @return Zend\Db\Sql\Delete

Sql对象接收$adapter,第二参数可以是表名。具体例子:

###返回一个Statement,然后调用它的execute方法返回结果集
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select();
$select->from('foo');
$select->where(array('id' => 2));

$statement = $sql->prepareStatementForSqlObject($select);
$results = $statement->execute();

###换成查询字符串,传递到适配器的query()方法
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter);
$select = $sql->select();
$select->from('foo');
$select->where(array('id' => 2));

$selectString = $sql->getSqlStringForSqlObject($select);
$results = $adapter->query($selectString, $adapter::QUERY_MODE_EXECUTE);


### 指定第二参数,不需要指定表。
use Zend\Db\Sql\Sql;
$sql = new Sql($adapter, 'foo');
$select = $sql->select();
$select->where(array('id' => 2)); // $select already has the from('foo') applied

这部分的内容,如果熟悉SQL,很容易就可以掌握,实际就是以面向对象的方法构建可以适配多平台的SQl。

可以使用以上例子那样使用Sql对象,不过大多配合TableGateway来使用。

可以直接实例化一个TableGateway对象,比如:

$tg = new Zend\Db\TableGateway\TableGateway(“test”,$adapter);

第一参数为表名,第二参数为适配器对象。

大部分情况下都不这样做,一般使用自己的类继承一下Zend\Db\TableGateway\TableGateway类,或者写一个公共类,然后所有类都从这个公共类继承。比如,我们可以把表和类对应起来,一一建立对应的TableGateway类,安装最佳实践,应该建立一个公共类,就算当前没有可以公共的方法(未来可能有),也是如此。每个表建立对应的TableGateway,我们就可以在类中添加复杂的业务逻辑(关注数据交互)。
tablegateway

构造函数在TableGateway中提供:

public function __construct($table,AdapterInterface $adapter){
	$this->table = $table;
	$this->adapter = $adapter;

	$this->sql = ($sql)?:new Sql($this->adapter,$this->table);

	$this->initialize();
}

可做如下封装:

// 常规封装
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct($adapter){
		parent::__construct($this->table, $adapter);
	}
}

// 这个注入方式简单粗暴
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		global $adapter;
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

// 或者使用
Registry::set(‘adapter’,$adapter);
class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		$adapter = Registry::get(‘adapter’);
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

// 更好的依赖注入
class Registry{
	public static $instance = array();
	
	public static function set($name,$callBack){
		if(!isset(self::$instance[$name])){
			self::$instance[$name] = $callBack;
		}
	}
	
	public static function get($name){
		if(isset(self::$instance[$name])){
			if(is_callable(self::$instance[$name])){
				$func = self::$instance[$name];
				self::$instance[$name] = $func();
			}
			return self::$instance[$name];
		}
		return false;
	}
}

Registry::set('adapter',function(){
	$adapter = new Zend\Db\Adapter\Adapter(array(
			'driver' => 'pdo',
			'dsn' => 'mysql:dbname=test;hostname=localhost',
			'username' => 'root',
			'password' => '',
			'driver_options' => array(
					PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
			),
	));
	return $adapter;
});

class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

	public function __construct(){
		$adapter = Registry::get(‘adapter’);
		if($adapter instanceof Zend\Db\Adapter\Adapter){
			parent::__construct($this->table, $adapter);
		}else{
			//抛异常
		}
	}
}

//最总改进范本
use Zend\Db\TableGateway\TableGateway;
use Zend\Db\Adapter\Adapter;
use Zend\Db\Adapter\AdapterInterface;
use Zend\Db\Sql\Sql;

class MyTableGateway extends Zend\Db\TableGateway\TableGateway{
	protected $table = ‘my_table_gateway’;

		public function __construct(AdapterInterface $adapter = null, $features = null, ResultSetInterface $resultSetPrototype = null, Sql $sql = null){
			if($adapter instanceof Adapter){
				parent::__construct($this->table, $adapter, $features, $resultSetPrototype, $sql);
			}else{
				$adapter = Registry::get('adapter');
				if($adapter instanceof Adapter){
					parent::__construct($this->table, $adapter);
				}else{
					throw new Exception("Need an Zend\Db\Adapter object.");
				}
			}
}
}

最后一种实现依赖注入的方法是现代框架常用的方式,我这里的实现是简化了而已。把资源注册到一个容器,然后在用到时初始化资源。在具体的类中,并不直接依赖外部的类对象,所谓的解耦的一种手段。数据库适配器初始化延迟到了具体需要使用这个资源的时候,有时候程序的运行,不需要实例化数据库适配器,那么就不要初始化它(初始化数据库资源是昂贵的)。

———————————————————————————–

$adapter = new Zend\Db\Adapter\Adapter(array(
    'hostname' => 'localhost',
    'database' => 'test',
    'username' => 'root',
    'password' => '',
    
    'driver' => 'mysqli',
    'charset' =>'utf8'
    
    //'driver' => 'pdo_mysql',
    //'driver_options' => array(
    //    PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES \'UTF8\''
    //),
));
$connection = $adapter->getDriver()->getConnection();
// connection还是具体数据库链接的抽象,它使用一个叫resource的名称代表最底层的数据库链接对象
// 通过getResource()获取这个数据库对象,然后可以调用具体的方法
$resource = $connection->getResource();
// 调用mysqli的具体方法
$resource->real_escape_string($);

以上程序为了能escape字符串而进行了不兼容调用,在Zend Framework新版本中,这个Bug已经修复,Zend\Db\Adapter\Platform\Mysql的quoteValue改为如下:

    public function quoteValue($value)
    {
        if ($this->resource instanceof DriverInterface) {
            $this->resource = $this->resource->getConnection()->getResource();
        }
        if ($this->resource instanceof \mysqli) {
            return '\'' . $this->resource->real_escape_string($value) . '\'';
        }
        if ($this->resource instanceof \PDO) {
            return $this->resource->quote($value);
        }
        return parent::quoteValue($value);
    }

Zend Framework 1.x Mysqli使用范例

由于项目需要,研究了一下Zend Framework 1.x Mysqli的驱动的使用(跟使用PDO的情况一样),大概浏览了一下Zend_Db包的源代码,同级别时代的实现,未发现可以超越Zend Framework的。原本打算自己来实现对应mysqli的封装的,大概研究了一下源代码,我就彻底放弃了。

set_include_path(implode(PATH_SEPARATOR, array(
    realpath(__DIR__ . '/../library'),
    get_include_path(),
)));

if(file_exists('../vendor/autoload.php')){
	$loader = include '../vendor/autoload.php';
}else{
	exit("Autoload Failed. ");
}
$loader->setUseIncludePath(true);

// $params = array(
// 		username	用户名		必须
// 		password	密码		必须
// 		dbname		数据库		必须

// 		port		端口号		
// 		socket		
// 		charset		字符集

// 		options = array(
// 				CASE_FOLDING				强制添加
// 				AUTO_QUOTE_IDENTIFIERS		强制添加
// 				FETCH_MODE					强制添加
// 				ALLOW_SERIALIZATION
// 				AUTO_RECONNECT_ON_UNSERIALIZE

// 		)
// 		driver_options = array()			参考mysqli_options()函数

// 		persistent	是否持久链接
// 		PROFILER	
// );

$params = array(
		'username' =>'root',
		'password' =>'',
		'dbname' =>'test',
		'charset' =>'utf8'
);
$db = Zend_Db::factory('MYSQLI', $params);
Zend_Db_Table_Abstract::setDefaultAdapter($db);

// 插入
$string = <<<'EOT'
'\'\" \\ \\ +
EOT;

for($i = 1; $i < 10; $i++){
	$db->insert('user', array('email'=>$string));
}

// 更新
$db->update('user', array('email'=>'xxxx'),'id=1');

// 删除
$db->delete('user','id=2');

// 查询
$result = $db->fetchAll("SELECT * FROM user");

print_r($result);
exit;

Zend Framework 2.* 事件管理器

大体模型:
ZendFramework2_EventManager

大部分时候都是直接使用Eventmanager,自定义的类要实现事件管理,可以实现EventManagerAwareInterface,要全局维护一个事件管理器,可以使用GlobalEventManager(单态设计模式),从模型来看,感觉有点设计过度~~

以下来自官方文档,作为理解事件管理器的入门资料:
Using the EventManager
This tutorial explores the various features of Zend\EventManager.

1 Terminology
An Event is a named action.
A Listener is any PHP callback that reacts to an event.监听器是一个和事件交互的回调函数。
An EventManager aggregates listeners for one or more named events, and triggers events. 事件管理器为一个或多个命名的事件聚集监听器,并触发事件。

Typically, an event will be modeled as an object, containing metadata surrounding when and how it was triggered, including the event name, what object triggered the event (the “target”), and what parameters were provided. Events are named, which allows a single listener to branch logic based on the event.
典型地,一个事件将建模为一个对象,包含了它何时和如何被触发的,包含事件名称,什么对象插入了这个时间(target,目录),和提供了什么参数这些元数据。Events are named, which allows a single listener to branch logic based on the event.

2 Getting started
The minimal things necessary to start using events are:

An EventManager instance
One or more listeners on one or more events
A call to trigger() an event

The simplest example looks something like this:

use Zend\EventManager\EventManager;

$events = new EventManager();
$events->attach('do', function ($e) {
    $event = $e->getName();
    $params = $e->getParams();
    printf(
        'Handled event "%s", with parameters %s',
        $event,
        json_encode($params)
    );
});

$params = array('foo' => 'bar', 'baz' => 'bat');
$events->trigger('do', null, $params);

//输出
Handled event "do", with parameters {"foo":"bar","baz":"bat"}

If you were paying attention to the example, you will have noted the null argument. Why is it there?

Typically, you will compose an EventManager within a class, to allow triggering actions within methods. The middle argument to trigger() is the “target”, and in the case described, would be the current object instance. This gives event listeners access to the calling object, which can often be useful.谁触发了事件。

use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;

class Example implements EventManagerAwareInterface
{
    protected $events;

    public function setEventManager(EventManagerInterface $events)
    {
        $events->setIdentifiers(array(
            __CLASS__,
            get_class($this)
        ));
        $this->events = $events;
    }

    public function getEventManager()
    {
        if (!$this->events) {
            $this->setEventManager(new EventManager());
        }
        return $this->events;
    }

    public function do($foo, $baz)
    {
        $params = compact('foo', 'baz');
        $this->getEventManager()->trigger(__FUNCTION__, $this, $params);
    }

}

$example = new Example();

$example->getEventManager()->attach('do', function($e) {
    $event  = $e->getName();
    $target = get_class($e->getTarget()); // "Example"
    $params = $e->getParams();
    printf(
        'Handled event "%s" on target "%s", with parameters %s',
        $event,
        $target,
        json_encode($params)
    );
});

$example->do('bar', 'bat');

—————
Example类中设置事件管理器,attach()方法往事件管理器中添加一个事件名称为do的事件,绑定到一个匿名函数。Example类中的do方法(注意,它实际给事件名称同名而已)内部调用事件管理的trigger方法触发do事件,第一个参数就是要触发的事件名称,中间的$this参数表示是谁触发的(上下文),后面的参数是传递给事件监听函数的参数。这些数据都可以在具体的监听器中取到($e->getTarget() $e->getParams())。
————–
The above is basically the same as the first example. The main difference is that we’re now using that middle argument in order to pass the target, the instance of Example, on to the listeners. Our listener is now retrieving that ($e->getTarget()), and doing something with it.

If you’re reading this critically, you should have a new question: What is the call to setIdentifiers() for? 调用setIdentifiers()为了啥?

3 Shared managers
One aspect that the EventManager implementation provides is an ability to compose a SharedEventManagerInterface implementation.一方面,事件管理器的实现提供了一个可以暴露SharedEventManagerInterface的实现(看例子吧,实在绕)

Zend\EventManager\SharedEventManagerInterface describes an object that aggregates listeners for events attached to objects with specific identifiers. It does not trigger events itself. Instead, an EventManager instance that composes a SharedEventManager will query the SharedEventManager for listeners on identifiers it’s interested in, and trigger those listeners as well. Zend\EventManager\SharedEventManagerInterface描述了一个以特定的标识符绑定到对象为事件聚集监听器的对象。 它本身不触发事件。相反,事件监听器实例暴露一个SharedEventManager,事件管理器向它查询它感兴趣的标识符的监听器,并且触发这些监听器。

How does this work, exactly?
Consider the following:

use Zend\EventManager\SharedEventManager;

$sharedEvents = new SharedEventManager();
$sharedEvents->attach('Example', 'do', function ($e) {
    $event  = $e->getName();
    $target = get_class($e->getTarget()); // "Example"
    $params = $e->getParams();
    printf(
        'Handled event "%s" on target "%s", with parameters %s',
        $event,
        $target,
        json_encode($params)
    );
});

This looks almost identical to the previous example; the key difference is that there is an additional argument at the start of the list, ‘Example’. This code is basically saying, “Listen to the ‘do’ event of the ‘Example’ target, and, when notified, execute this callback.”监听Example上的do事件,当触发时,执行回调。

This is where the setIdentifiers() argument of EventManager comes into play. The method allows passing a string, or an array of strings, defining the name or names of the context or targets the given instance will be interested in. If an array is given, then any listener on any of the targets given will be notified.方法setIdentifiers()可以是字符串,字符串数组,

So, getting back to our example, let’s assume that the above shared listener is registered, and also that the Example class is defined as above. We can then execute the following:

$example = new Example();
$example->getEventManager()->setSharedManager($sharedEvents);
$example->do('bar', 'bat');

and expect the following to be echo‘d:

Handled event "do" on target "Example", with parameters {"foo":"bar","baz":"bat"}

Now, let’s say we extended Example as follows:

class SubExample extends Example
{
}

One interesting aspect of our setEventManager() method is that we defined it to listen both on __CLASS__ and get_class($this). This means that calling do() on our SubExample class would also trigger the shared listener! It also means that, if desired, we could attach to specifically SubExample, and listeners attached to only the Example target would not be triggered.

Finally, the names used as contexts or targets need not be class names; they can be some name that only has meaning in your application if desired. As an example, you could have a set of classes that respond to “log” or “cache” – and listeners on these would be notified by any of them.

————————-

//例子
use Zend\EventManager\EventManager;
use Zend\EventManager\EventManagerAwareInterface;
use Zend\EventManager\EventManagerInterface;
use Zend\EventManager\SharedEventManager;

class Example implements EventManagerAwareInterface
{
	protected $events;
	public function setEventManager(EventManagerInterface $events)
	{
		$events->setIdentifiers(array(
				__CLASS__,
				get_class($this)
		));
		$this->events = $events;
	}

	public function getEventManager()
	{
		if (!$this->events) {
			$this->setEventManager(new EventManager());
		}
		return $this->events;
	}

	public function doo($foo, $baz)
	{
		$params = compact('foo', 'baz');
		$this->getEventManager()->trigger(__FUNCTION__, $this, $params);
	}

}

class SubExample extends Example
{
}

$example = new Example();
$subexample = new SubExample();

$example->getEventManager()->attach('doo', function($e) {
	$event  = $e->getName();
	$target = get_class($e->getTarget()); // "Example"
	$params = $e->getParams();
	printf(
		'Handled event "%s" on target "%s", with parameters %s',
		$event,
		$target,
		json_encode($params)
	);
});

$sharedEvents = new SharedEventManager();
$sharedEvents->attach('Example', 'doo', function ($e) {
	$event  = $e->getName();
	$target = get_class($e->getTarget()); // "Example"
	$params = $e->getParams();
	printf(
		'Handled event "%s" on target "%s", with parameters %s',
		$event,
		$target,
		json_encode($params)
	);
});
$example->getEventManager()->setSharedManager($sharedEvents);

// 触发两个监听函数
$example->doo('bar', 'bat');

echo "\n-----------------\n";
$subexample->doo('subbar','subbat');

echo "\n-----------------\n";
$example->getEventManager()->trigger('doo',new stdClass(),array('1','2'));

//输出
Handled event "doo" on target "Example", with parameters {"foo":"bar","baz":"bat"}Handled event "doo" on target "Example", with parameters {"foo":"bar","baz":"bat"}
-----------------
Handled event "doo" on target "SubExample", with parameters {"foo":"subbar","baz":"subbat"}
-----------------
Handled event "doo" on target "stdClass", with parameters ["1","2"]Handled event "doo" on target "stdClass", with parameters ["1","2"]

研究一下输出,就能明白,在trigger时,如果指定了target,就去取回这些target下对应事件绑定的函数,在加上全局的(没有指定target)的,取回执行。
————————-

4 Wildcards
So far, with both a normal EventManager instance and with the SharedEventManager instance, we’ve seen the usage of singular strings representing the event and target names to which we want to attach. What if you want to attach a listener to multiple events or targets?

The answer is to supply an array of events or targets, or a wildcard, *.

Consider the following examples:

// Multiple named events:
$events->attach(
    array('foo', 'bar', 'baz'), // events
    $listener
);

// All events via wildcard:
$events->attach(
    '*', // all events
    $listener
);

// Multiple named targets:
$sharedEvents->attach(
    array('Foo', 'Bar', 'Baz'), // targets
    'doSomething', // named event
    $listener
);

// All targets via wildcard
$sharedEvents->attach(
    '*', // all targets
    'doSomething', // named event
    $listener
);

// Mix and match: multiple named events on multiple named targets:
$sharedEvents->attach(
    array('Foo', 'Bar', 'Baz'), // targets
    array('foo', 'bar', 'baz'), // events
    $listener
);

// Mix and match: all events on multiple named targets:
$sharedEvents->attach(
    array('Foo', 'Bar', 'Baz'), // targets
    '*', // events
    $listener
);

// Mix and match: multiple named events on all targets:
$sharedEvents->attach(
    '*', // targets
    array('foo', 'bar', 'baz'), // events
    $listener
);

// Mix and match: all events on all targets:
$sharedEvents->attach(
    '*', // targets
    '*', // events
    $listener
);

5 Listener aggregates
6 Introspecting results
7 Short-circuiting listener execution
8 Keeping it in order
9 Custom event objects

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

Zend Framwork 1.x Zend_Session组件

Zend Framwork 1.x Zend_Session组件是对PHP SESSION操作的封装。这层封装总体来说还是相当不错的。对SESSION的操作用Zend_Session来操作,看例子:

		Zend_Session::setOptions(
			array(
				"strict"	=>"on",
				"name" 		=>"YaaSessionID"
			)
		);
		Zend_Session::start();
		$yaaAuth = new Zend_Session_Namespace('YaaAuth');
		
		if(!isset($yaaAuth->securityToken)) {
			$yaaAuth->securityToken = md5(uniqid(rand(),true));
		}

可以通过Zend_Session::setOptions()来设置SESSION的参数,这些个参数基本和php.ini中SESSION的参数想对应。实际这个配置数组也可以在Zend_Session::start()时传递,不过Zend_Session::start()可以接收布尔值表示是否使用strict模式。所谓的strict就是指要显示的调用Zend_Session::start(),表示显式启动会话。在new一个Zend_Session_Namespace时,在没有启用strict模式的情况下,它的内部会默认调用一次Zend_Session::start(),否则就是不调用(所以要主动调用Zend_Session::start())。

Zend_Session_Namespace基本是$_SESSION的映射,不过它提供了一个命名空间的管理方式,实际并没有多么的高深,虽然方便,但是这个封装显得有点笨重。从原理上看,每次new一个Zend_Session_Namespace后,对这个实例的操作都是映射到$_SESSION。

我们知道,SESSION默认存在文件系统中,也可以存储在其它介质中,这个是通过修改savehander来实现的。1.x版本中仅仅提供了数据库的实现。

在验证器上,提供了验证UA的实现。

在Zend Framework 2.x中,Zend_Session提供了一个更加完整封装。

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

Zend Framework 1.x 自定义 MVC 流程

Zend Framework 1.x 提供了Zend_Application组件实现一个MVC流程,实际可以完全可以抛开而自定义一个MVC流程,比如我希望把模块名称分别在视图和控制器和模型文件夹中进行组织:
directory

把网站指定的public目录下并添加.htaccess文件:

RewriteEngine On
RewriteCond %{REQUEST_FILENAME} -s [OR]
RewriteCond %{REQUEST_FILENAME} -l [OR]
RewriteCond %{REQUEST_FILENAME} -d
RewriteRule ^.*$ - [NC,L]
RewriteRule ^.*$ index.php [NC,L]

自定义index.php文件(这里面的各个部分可以封装起来):

<?php
// 根目录
defined('APP_PATH')
|| define('APP_PATH', realpath(dirname(__FILE__) . '/..'));

// 搜索路径
set_include_path(
        implode(
                PATH_SEPARATOR,
                array(
                        realpath(APP_PATH . '/models'),
                        realpath(APP_PATH . '/plugins'),
                        realpath(APP_PATH . '/library'),
                        get_include_path(),
                )
        )
);

// 加载配置
require_once APP_PATH.'/configs/config.php';

// 自动类装载
require_once 'Zend/Loader/Autoloader.php';
$loader = Zend_Loader_Autoloader::getInstance();
$loader->setFallbackAutoloader(true);

// 前端控制器配置
$front = Zend_Controller_Front::getInstance();
$front->setParam("noViewRenderer", true);
$front->returnResponse(true);

// 多模块
$modules = array();
foreach($config['modules'] as $module=>$path){
        $modules[$module] = APP_PATH.$path;
}
if(empty($modules) || !isset($modules['default'])){
        $modules['default'] = APP_PATH.'/controllers/defalut';
}
$front->setControllerDirectory($modules);

// 注册插件
foreach($config['plugins'] as $plugin){
//      $front->registerPlugin(new $plugin);
}

// 设置路由
$router = $front->getRouter();

// 设置数据库
$params = $config['db'];
$db = Zend_Db::factory("Pdo_Mysql",$params);
Zend_Db_Table::setDefaultAdapter($db);
//$db->query("SET NAMES UTF8");

Zend_Registry::set("db", $db);

// 设置布局
$layout = new Zend_Layout();
$layout->setLayoutPath(APP_PATH."/views/layouts");
Zend_Registry::set("layout", $layout);

// 设置视图
$view = new Zend_View();
//$view->setBasePath(APP_PATH."/views");
$view->setScriptPath(APP_PATH."/views/scripts");
Zend_Registry::set("view",$view);

// 前端控制器分发
$response = $front->dispatch();

// 输出
$response->sendHeaders();
echo $response->getBody();

这里指定了控制器目录,关闭了默认视图,自定视图等,通过一个conf.php文件引入配置:

$config = array(
		"db" => array(
				"host"=>"localhost",
				"username"=>"root",
				"password"=>"root",
				"dbname"=>"test",
		),
		"modules" => array(
				"default"=>"/controllers/default",
				"admin"=>"/controllers/admin"
		),
		"plugins" => array("MyPlugin"),
);

可能会觉得为何不使用XML或者INI文件而产生疑惑,原因在于原生的PHP数组不需要进行任何转换,它是最高效的办法。

控制器 和 视图:

//controllers/default/IndexController.php
<?php
class IndexController extends Zend_Controller_Action{
        public function init(){
                $this->db = Zend_Registry::get("db");
                $this->view = Zend_Registry::get("view");
        }
        public function indexAction(){
        }
        public function testAction(){
                $emails = $this->db->fetchAll("SELECT * FROM emails ORDER BY rand() LIMIT 10");

                $this->view->emails = $emails;

                echo $this->view->render("default/index/index.phtml");
        }
}

//views/script/default/index/index.phtml
<?php
print_r($this->emails);

以上代码可以正确工作。

设置了多模块之后,控制器名称需要由 模块名+”_”+控制器名称+”Controller“组成。比如:

<?php
class Admin_IndexController extends Zend_Controller_Action{}

这里的首字母是大写的,实际上换成小写的也可以工作,它必须不区分大小写,因为它需要对应URI:admin/index,但是中间的下划线是必须的,同理,控制器中的Action方法应该也是不区分大小写的,实际上在确定Action方法是都被转换成小写字母,所以如果定义的方法有大写字母,那么在比较的时候讲无法匹配,那么就认为这个方法不存在,所以Action方法不应该包含大写字母。

在自定义Zend_View时,有两个方法需要注意,视图有基准路径和脚本路径的概念,用setBasePath设置基准路径,所有脚本都放到这个目录(默认放入到scripts目录),可以用setScriptPath设置脚本放置的目录(或者不指定基路径,这里直接指定绝对路径),也可以调用addScriptPath添加多个脚本路径。

另外一种非常有用的方法是插件,当要把代码注入到某个MVC流程时,它就非常有用:

class MyPlugin extends Zend_Controller_Plugin_Abstract{
	public function preDispatch(Zend_Controller_Request_Abstract $request){
		echo "predispatch call().";
	}
}
// 为了使插件可用
$front->registerPlugin(new MyPlugin());

Zend Framework 1.X Zend_Json组件

PHP 5.2开始支持json_encode()和json_decode()这两个函数。实际上,这两个函数已经可以工作的非常好了,Zend Framework 1.X中Zend_Json组件实现的两个基本方法,默认是对它们的包装而已,不过提供了一个叫把xml装换成json的函数:

public function jsonAction(){
	$xml = "<?xml version='1.0'?>
<root>
  	<ele name='id_1' id='1'>1</ele>
	<ele name='id_2' id='2'>2</ele>
	<bok name='test' id='tttt'>
		<one ids='111' name='vfeelit'>111</one>
		<one ids='222' name='ifeeline'>222</one>
		<two name='id22'>2</two>
	</bok>
</root>";
        // XML  -> Json
	$js = Zend_Json::fromXml($xml,false);
        // Json -> PHP变量
	$js = Zend_Json::decode($js);
	print_r($js);
		
	echo $js['root']['ele'][0]['@attributes']['name'];		
}

////////////////////////////
// 输出
Array
(
    [root] => Array
        (
            [ele] => Array
                (
                    [0] => Array
                        (
                            [@attributes] => Array
                                (
                                    [name] => id_1
                                    [id] => 1
                                )

                            [@text] => 1
                        )

                    [1] => Array
                        (
                            [@attributes] => Array
                                (
                                    [name] => id_2
                                    [id] => 2
                                )

                            [@text] => 2
                        )

                )

            [bok] => Array
                (
                    [one] => Array
                        (
                            [0] => Array
                                (
                                    [@attributes] => Array
                                        (
                                            [ids] => 111
                                            [name] => vfeelit
                                        )

                                    [@text] => 111
                                )

                            [1] => Array
                                (
                                    [@attributes] => Array
                                        (
                                            [ids] => 222
                                            [name] => ifeeline
                                        )

                                    [@text] => 222
                                )

                        )

                    [two] => Array
                        (
                            [@attributes] => Array
                                (
                                    [name] => id22
                                )

                            [@text] => 2
                        )

                    [@attributes] => Array
                        (
                            [name] => test
                            [id] => tttt
                        )

                )

        )

)
id_1

Zend_Json::fromxml把XML转换为Json字符串,方法第一参数自然是XML字符串了,第二参数是控制是否读取XML元素的属性,默认为true表示忽略属性。如果要把XML转换成Json字符串,这的确是一个比较好封装,如果要把XML转换成PHP数组,再经过一次转换也是一个办法。

Zend_Json组件一般直接使用Zend_Json::decode()、Zend_Json::encode()和Zend_Json::fromxml()三个方法。

Zend Framework 1.X 使用Zend_Auth组件实现登录

先来一段示例代码:

    	if ($this->getRequest()->isPost()){
        	if ($formLogin->isValid($_POST)){
        		$data = $formLogin->getValues();
        		
        		// 取得默认的数据库适配器
        		$db = Zend_Db_Table::getDefaultAdapter();
        		// 实例化一个auth适配器
        		$authAdapter = new Zend_Auth_Adapter_DbTable($db, 'core_users', 'username', 'password');
        		// 设置认证用户名和密码
        		$authAdapter->setIdentity($data['username']);
        		$authAdapter->setCredential(md5($data['password']));
        		// 实现authenticate方法
        		$result = $authAdapter->authenticate();
        		if ($result->isValid()){
        			// 获得getInstance实例
        			$auth = Zend_Auth::getInstance();
        			// 存储用户信息
        			$storage = $auth->getStorage();
        			$storage->write($authAdapter->getResultRowObject(
        				array('id', 'username', 'role')
        			));
        			$id = $auth->getIdentity()->id;// 获取用户id
        			// 记录登录时间
        			$modelUser = new Kh_Model_User();
        			$loginTime = $modelUser->loginTime($id);
        			
        			return $this->_redirect('/user/account/id/'.$id);
        		}
        		else{
        			$this->view->loginMessage = "对不起,你的用户名或密码不符。";
        		}
        	}
        }

Zend_Auth_Adapter_DbTable设置一个认证适配器,所谓认证适配器说得直接点就是存放了所有用户与用户证书(密码)的地方,要认证一个用户和对应的证书是否合法,就要使用这个适配器来帮助完成。认证适配器需要实现Zend_Auth_Adapter_Interface接口,它只有一个方法:authenticate(),一般来说,认证适配器可以是很宽松的,Zend_Auth组件提供了Zend_Auth_Adapter_DbTable、Zend_Auth_Adapter_Digest、Zend_Auth_Adapter_Http、Zend_Auth_Adapter_Ldap、Zend_Auth_Adapter_OpenId。使用Zend_Auth_Adapter_DbTable意思是把认证信息放入一张数据表中,这个还是非常经典的引用场景。

以使用Zend_Auth_Adapter_DbTable为例子,它的构造函数有四个参数,首先是数据库适配器,因为它需要连接数据库,然后是数据库中的哪个数据表,然后是数据表对应的用户名和证书(密码)字段。

实例化这个适配器之后,还需要设置要比对的用户名(用户认证标识)和证书(密码):

$authAdapter->setIdentity($data['username']);
$authAdapter->setCredential(md5($data['password']));

然后就是调用authenticate()方法,这个方法完成链接数据库查询比对数据,这个方法返回一个Zend_Auth_Result实例,如果因为某些原因认证查询不能执行,authenticate()应该抛出一个由Zend_Auth_Adapter_Exception产生的异常。 Zend_Auth_Result实际只有4个可用方法:

$result->getCode();       //错误代码
$result->getIdentity();   //尝试认证的用户名
$result->getMessages();   //错误信息
$result->isValid();       //是否通过认证

其中getCode()可能为如下值:

Zend_Auth_Result::SUCCESS
Zend_Auth_Result::FAILURE
Zend_Auth_Result::FAILURE_IDENTITY_NOT_FOUND
Zend_Auth_Result::FAILURE_IDENTITY_AMBIGUOUS
Zend_Auth_Result::FAILURE_CREDENTIAL_INVALID
Zend_Auth_Result::FAILURE_UNCATEGORIZED

对应成功,失败,用户名不存在失败,用户名模糊失败(???),密码错误失败,为分类失败。

如果认证成功,需要持久化存储认证结果,这个一般都是通过SESSION实现的。Zend_Auth组件也是如此。Zend_Auth组件默认使用Zend_Auth_Storage_Session类来实现持久化存储,它内部使用Zend_Session,真实的过程是它使用Zend_Session_Namespace来注册一个’Zend_Auth’名空间,然后存储到这个名空间中。

Zend_Auth组件可以通过Zend_Auth::setStorage()来设置一个自定义的持久化存储类实例(这个类必须实现Zend_Auth_Storage_Interface接口),比如:

$auth = Zend_Auth::getInstance();

$auth->setStorage(new Zend_Auth_Storage_Session('someNamespace'));

这样,它就会使用你指定的实例,并且改变了要注册的名空间(一般都不需要这么干)。默认只要通过调用getStorage()就会默认取得一个持久化存储对象(就是new Zend_Auth_Storage_Session(‘Zend_Auth’)),然后只要调用它的write()方法就可以包数据写入到Zend_Auth这个会话名空间中。

以上代码调用了适配器的getResultRowObject()方法,传递了一个数组,意思就是要从已经获取认证的那行数据中,获取指定的数据,它返回一个stdClass对象,把这个对象写入到会话中。

Zend Framework 1.X Zend_Mail组件

通用例子:

set_time_limit(0);

$mailTransport = new Zend_Mail_Transport_Smtp('smtp.qq.com',array(
	'auth'=>'login',
	'username'=>'xxxx@qq.com',
	'password'=>'xxxxxxxxx',
	'ssl'=>'ssl'
));

$mail = new Zend_Mail('utf-8');					
$mail->setBodyHtml('<p>邮件主体</p>');
$mail->setSubject('邮件主题');
$mail->setFrom('from@qq.com','vfeelit');
$mail->addTo(to@qq.com,'ifeeline');
$mail->send($mailTransport);

浏览一下Zend_Mail的构造函数:

class Zend_Mail extends Zend_Mime_Message
{
    /**
     * @var Zend_Mail_Transport_Abstract
     * @static
     */
    protected static $_defaultTransport = null;

    /**
     * Mail character set
     * @var string
     */
    protected $_charset = 'iso-8859-1';
    public function __construct($charset = null)
    {
        if ($charset != null) {
            $this->_charset = $charset;
        }
    }
    public static function setDefaultTransport(Zend_Mail_Transport_Abstract $transport)
    {
        self::$_defaultTransport = $transport;
    }
    public function send($transport = null)
    {
        if ($transport === null) {
            if (! self::$_defaultTransport instanceof Zend_Mail_Transport_Abstract) {
                require_once 'Zend/Mail/Transport/Sendmail.php';
                $transport = new Zend_Mail_Transport_Sendmail();
            } else {
                $transport = self::$_defaultTransport;
            }
        }
	// .......
	// 实际调用Transport的send方法发送
        $transport->send($this);

        return $this;
    }

构造函数处理设置编码,没有其它的动作。注意,默认编码是iso-8859-1。默认的Transport为空,如果在调用send()方法时没有指定Transport,那么默认的Transport就会被调用,所以可以在实例化Zend_Mail后调用setDefaultTransport()设置默认的Transport,这样发送邮件就不需要指定Transport了,如果默认的Transport不是Zend_Mail_Transport_Abstract实例,最终会实例化一个Zend_Mail_Transport_Sendmail()作为Transport。

Zend_Mail组件中实际提供了三个Transport:Zend_Mail_Transport_Sendmail,Zend_Mail_Transport_Smtp,Zend_Mail_Transport_File。

Zend_Mail_Transport_Sendmail是通过Sendmail发送邮件,而Zend_Mail_Transport_Smtp可以通过连接Smtp服务器来发送邮件。

通过以上分析可知,要发送邮件,应该先初始化一个Zend_Mail_Transport_Abstract类实例(Zend_Mail_Transport_Smtp),然后实例化一个Zend_Mail实例,它是邮件内容等信息在载体,可以调用它的setDefaultTransport()先设置一个默认的Transport,也可以在send时指定一个Transport,在send时指定Transport在要通过不同Transport发送邮件时非常有用:

$tr1 = new Zend_Mail_Transport_Smtp('server@example.com');
$tr2 = new Zend_Mail_Transport_Smtp('other_server@example.com');

$mail = new Zend_Mail();
$mail->send($tr1);
$mail->send($tr2);
$mail->send();  // use default again

如果要通过Transport一次发送多封邮件,参考以下例子:

$tr = new Zend_Mail_Transport_Smtp('mail.example.com');

$mail = new Zend_Mail();
Zend_Mail::setDefaultTransport($tr);

$tr->connect();
for ($i = 0; $i < 5; $i++) {
	$mail->send();
}
$tr->disconnect();

Zend_Mail组件对发送邮件关键的内容就这些,对应调用相应方法即可。

Zend_Mail组件还提供了收邮件的内容,具体参考官方手册。

Zend Framework 1.X 多模块支持

Zend Framework 1.X 支持多模块,要开启多模块支持,实际非常的简单。
Zend Framework 1.X多模块

把模块放入modules中,为了使得模块可用,有如下方法:

// 1
$front->setControllerDirectory(array(
      'default' => '/path/to/application/modules/default/controllers',
      'admin'    => '/path/to/application/modules/admin/controllers'
));

// 2
//第二参数指定模块名,如果没有指定,说明是设置default模块路径
$front->addControllerDirectory('/path/to/application/modules/default/controllers', 'default');
$front->addControllerDirectory('/path/to/application/modules/admin/controllers', 'admin');

// 3
$front->addModuleDirectory('/path/to/application/modules');

以上三种方法都可以设置模块。其中最后一种最为简单,通过调用addModuleDirectory方法即可。对于第三种方法,对应的模块中必须存在controllers目录,如果要改变使用不同的目录,可以使用:

$front->setModuleControllerDirectoryName('con');

这样,模块的控制器就可以存放在con目录中。如果使用Zend Framework的Zend_Application启动(标准),那么就可以通过配置文件修改这个过程,相关代码为Zend_Application_Resource_Frontcontroller的init方法:

   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':
                    $front->setModuleControllerDirectoryName($value);
                    break;

                case 'moduledirectory':
                    if (is_string($value)) {
                        $front->addModuleDirectory($value);
                    } elseif (is_array($value)) {
                        foreach($value as $moduleDir) {
                            $front->addModuleDirectory($moduleDir);
                        }
                    }
                    break;

                case 'defaultcontrollername':
                    $front->setDefaultControllerName($value);
                    break;

根据设置controllerdirectory的不同,会分别调用setControllerDirectory和addControllerDirectory; moduledirectory指令调用addModuleDirectory方法;还可以看到modulecontrollerdirectoryname设置模块控制器目录名,所以配置中可以如下设置:

//调用addControllerDirectory
resources.frontController.controllerDirectory.default = APPLICATION_PATH "/modules/default"
resources.frontController.controllerDirectory.admin = APPLICATION_PATH "/modules/admin"

//调用addModuleDirectory
resources.frontController.moduledirectory = APPLICATION_PATH "/modules"

这两种配置方式只要二选一即可。如果调用了addModuleDirectory方法,表示所有模块都在这个目录下,显然,这个目录下必须存在default模块,除非设置了defaultmodule或显式调用了setDefaultModule方法改变了默认模块。所以,如果希望默认的模块放在默认位置,自定义模块放入自定义目录,只要针对自定义模块调用addControllerDirectory方法,default模块指定到APPLICATION下即可(必须指定default模块)。另外,如果是单模块(默认是default模块),只要调用setControllerDirectory或者在配置中直接赋值给resources.frontController.controllerDirectory即可(实际是调用setControllerDirectory),它会把这个唯一的模块设置为默认模块,并固定名字为default。

以上设置只是让控制和视图可以工作,但是模块中模型还是无法使用的,因为不知道如何自动装载这些资源。每个模块,都可以提供一个Bootstrap文件,类似:

class Admin_Bootstrap extends Zend_Application_Module_Bootstrap
{

	public function init()
    {

    }
    
}

注意这个Bootstrap跟一般的Bootstrap是不一样的,它是继承Zend_Application_Module_Bootstrap,说明它主要是用来设置模块的。这个类基本上你不需要再写其它的代码了,不过这时候还是无法使用模块的模型,还需要在配置中添加:

resources.modules[] = ""

这一行的代码会触发Zend_Application_Resource_Modules类的init方法被执行,它会负责搜索所有模块找到每个模块下的Bootstrap类,然后调用bootstrap()方法,最终会把模块相关模块的目录结构做一遍映射。

关于URL访问:
1 当没有指定模块名时,默认会定位到default模块的默认控制器和默认ACTION
2 当指定模块名时,如果没有指定控制器和动作,默认会在本模块下寻找默认控制器(默认设置为index),如果默认控制器找不到就会抛出异常(如果指定控制器不存在也一样),但是如果设置了前端控制setParam(‘useDefaultControllerAlways’, true),那么就会定向到default模块,否则就是404。

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

Zend Framework 1.X Application对象构建

//index.php
$application = new Zend_Application(
    APPLICATION_ENV,
    APPLICATION_PATH . '/configs/application.ini'
);

//Zend_Application
$_autoloader 	装载器
$_bootstrap	引导程序
$_environment	环境(开发 or 生产)
$_optionKeys	配置字段
$_options	选项

实际这个对象重点在$_bootstrap。

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);
/***
 *关键代码
    public function setOptions(array $options)
    {
        /*
        application.ini文件添加,对应加载文件并合并
        config.exta = APPLICATION_PATH "/configs/a.ini"
        config.extb = APPLICATION_PATH "/configs/b.ini"
        */
        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);

	//设置_optionKeys
        $this->_optionKeys = array_keys($options);

	//配置PHP配置值 实际调用init_set方法
        if (!empty($options['phpsettings'])) {
            $this->setPhpSettings($options['phpsettings']);
        }
	
	//配置includepaths,可以把自定义搜索路径放入
        if (!empty($options['includepaths'])) {
            $this->setIncludePaths($options['includepaths']);
        }
	
	//通过autoloadernamespaces设置名空间
	//这里只是把名空间注册到autoloader中
        if (!empty($options['autoloadernamespaces'])) {
            $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);
            }
        }

	//设置Bootstrap
        if (!empty($options['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'];
                }

                $this->setBootstrap($path, $class);
            } else {
                throw new Zend_Application_Exception('Invalid bootstrap information provided');
            }
        }

        return $this;
    }
*/
        }
    }

设置$_autoloader,$_environment,$_options。可以看到,没有明确设置$_bootstrap,实际上$_bootstrap是可以自定义的,所以它会根据配置生成,具体就是在调用setOptions()方法时生成的,$_optionKeys也在这个方法中被设置。如果不知道配置文件怎么配置,看看这个方法。

这个对象提供了一些有用的方法,比如getAutoloader()获取实例化的装载器;getBootstrap()和setBootstrap($path, $class = null)是获取和设置Bootstrap对象(如果没有则使用一个默认的);getEnvironment()获取应用的环境;getOption($key) 和 getOptions()和hasOption($key)和mergeOptions(array $array1, $array2 = null)和setOptions(array $options)是对配置值的操作;setAutoloaderNamespaces()是对装载器的封装;setIncludePaths(array $paths)添加路径到include_path;setPhpSettings(array $settings, $prefix = ”)修改php.ini的配置值。

以上的Application对象的构建过程虽然很简单,但是需要认真理解清楚,特别是setOptions()方法,所以以下分几个部分:

//setOptions()方法
        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);

这段代码高速我们,可以在配置文件中通过config指令,把在其它地方的配置加载进来。也就是说可以把配置打散到多个配置文件中,然后通过主配置文件的config指令把它们汇集起来。不过这里有个细节需要知道,Application的构造函数第一个参数是设置运行环境(开发 or 生产,或其它),比如是development,这个值和获取配置中的哪个段的配置相关联,配置文件必须存在这个段,否则将产生解析文件错误。

        if (!empty($options['phpsettings'])) {
            $this->setPhpSettings($options['phpsettings']);
        }

这个可以动态修改php.ini中的配置值(当然是可以修改的部分),内部调用ini_set函数,比如:

phpSettings.display_startup_errors = 0
phpSettings.display_errors = 0

//等同
ini_set('display_startup_errors',0);
ini_set('display_errors',0);
        if (!empty($options['includepaths'])) {
            $this->setIncludePaths($options['includepaths']);
        }

可以通过includepaths指定要添加到include_path中的路径。

        if (!empty($options['autoloadernamespaces'])) {
            $this->setAutoloaderNamespaces($options['autoloadernamespaces']);
        }

通过使用autoloadernamespaces来注册名空间到自动装载器。

        if (!empty($options['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'];
                }

                $this->setBootstrap($path, $class);
            } else {
                throw new Zend_Application_Exception('Invalid bootstrap information provided');
            }
        }

设置Bootstrap。

另外,再去看下Bootstrap对象的setOptions()方法,就能大体把握配置文件能进行哪些配置了。