温斯顿吴 赌场式交易员

《深入PHP-面向对象、模式与实践》读书笔记

2017-04-05

《深入PHP——面向对象、模式与实践》读书笔记

单例模式

保证某个类在整个应用中仅有一个实例,并提供一个访问它的全局访问点。 单例模式的要点有三个:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

<?php

class Preferences {
    private $props = array();
    private static $instance;

    private function __construct() { }	

    public static function getInstance() {
        if ( empty( self::$instance ) ) {
            self::$instance = new Preferences();
        }
        return self::$instance;
    }

    public function setProperty( $key, $val ) {
        $this->props[$key] = $val;
    }

    public function getProperty( $key ) {
        return $this->props[$key];
    }
}


$pref = Preferences::getInstance();
$pref->setProperty( "name", "matt" );

unset( $pref ); // 删除$pref

$pref2 = Preferences::getInstance();
print $pref2->getProperty( "name" ) ."\n"; 	// 虽然$pref被删除了,但是值被保留了
?>

工厂方法模式

工厂方法模式把创建者类与要生产的产品类分离开来,创建者是一个工厂类,其中定义了用于生产产品对象的类方法。创建者的每个子类实例化一个相应的产品子类。 例如:有一个编码器类ApptEncoder,用来将数据转换成特定的格式。此外有一个管理类CommsManager,用来获取不同的编码器。 一种比较差的、基于条件语句的实现如下:

<?php
abstract class ApptEncoder {
    abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder {
    function encode() {
        return "Appointment data encoded in BloggsCal format\n";
    }
}

class MegaApptEncoder extends ApptEncoder {
    function encode() {
        return "Appointment data encoded in MegaCal format\n";
    }
}

class CommsManager {
    const BLOGGS = 1;
    const MEGA = 2;
    private $mode ;

    function __construct( $mode ) {
        $this->mode = $mode;
    }

    function getHeaderText() {					// 基于条件语句
        switch ( $this->mode ) {
            case ( self::MEGA ):
                return "MegaCal header\n";
            default:
                return "BloggsCal header\n";
        }
    }
    function getApptEncoder() {					// 基于条件语句(重复判断)
        switch ( $this->mode ) {
            case ( self::MEGA ):
                return new MegaApptEncoder();
            default:
                return new BloggsApptEncoder();
        }
    }

// 如果再加入一个新的getFooterText()方法,则要再多一次条件判断
}

$man = new CommsManager( CommsManager::MEGA );
print ( get_class( $man->getApptEncoder() ) )."\n";
$man = new CommsManager( CommsManager::BLOGGS );
print ( get_class( $man->getApptEncoder() ) )."\n";
?>

工厂方法的实现:

<?php
abstract class ApptEncoder {
    abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder {
    function encode() {
        return "Appointment data encode in BloggsCal format\n";
    }
}

abstract class CommsManager {
    abstract function getHeaderText();
    abstract function getApptEncoder();
    abstract function getFooterText();
}

// 产品和工厂都抽象出来,一个工厂的子类对应一个产品子类
class BloggsCommsManager extends CommsManager {
    function getHeaderText() {
        return "BloggsCal header\n";
    }

    function getApptEncoder() {
        return new BloggsApptEncoder();
    }

    function getFooterText() {
        return "BloggsCal footer\n";
    }
}
?>

抽象工厂模式

工厂方法解决了同一个编码器添加不同编码格式的问题,当需要添加不同的编码器时,需要使用抽象工厂模式。 例如需要添加一个新的解码器类:TtdEncoder,该类同样支持ApptEncoder所支持的所有编码格式。

<?php
abstract class ApptEncoder {
    abstract function encode();
}

class BloggsApptEncoder extends ApptEncoder {
    function encode() {
        return "Appointment data encoded in BloggsCal format\n";
    }
}

class MegaApptEncoder extends ApptEncoder {
    function encode() {
        return "Appointment data encoded in MegaCal format\n";
    }
}

// TtdEncoder及其子类的实现略

abstract class CommsManager {
    abstract function getHeaderText();
    abstract function getApptEncoder();		// 解码器类型1 
    abstract function getTtdEncoder();			// 解码器类型2
    abstract function getContactEncoder();		// 解码器类型3
    abstract function getFooterText();
}

class BloggsCommsManager extends CommsManager {
    function getHeaderText() {
        return "BloggsCal header\n";
    }

    function getApptEncoder() {
        return new BloggsApptEncoder();
    }

    function getTtdEncoder() {
        return new BloggsTtdEncoder();
    }

    function getContactEncoder() {
        return new BloggsContactEncoder();
    }

    function getFooterText() {
        return "BloggsCal footer\n";
    }
}

class MegaCommsManager extends CommsManager {
    function getHeaderText() {
        return "MegaCal header\n";
    }

    function getApptEncoder() {
        return new MegaApptEncoder();
    }

    function getTtdEncoder() {
        return new MegaTtdEncoder();
    }

    function getContactEncoder() {
        return new MegaContactEncoder();
    }

    function getFooterText() {
        return "MegaCal footer\n";
    }
}

/*
$mgr = new MegaCommsManager();
print $mgr->getHeaderText();
print $mgr->getApptEncoder()->encode();
print $mgr->getFooterText();
*/
?>

工厂方法:一个抽象产品类可以派生出多个具体产品类,一个抽象工厂类可以派生出多个具体工厂类,每个具体工厂类只能创建一个具体产品类的实例。 抽象工厂:多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。 如上例,产品有编码器、编码格式两个维度,工厂的不同子类对应不同的编码格式,每个子类提供该格式的不同类型的编码器。

原型模式

用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。 抽象工厂模式实现了平行的继承扩展,但当产品类型过多时,往往需要实现一个庞大的继承体系,变得不灵活。这时可以使用抽象工厂的变形:原型模式。

<?php

class Sea {}
class EarthSea extends Sea {}
class MarsSea extends Sea {}

class Plains {}
class EarthPlains extends Plains {}
class MarsPlains extends Plains {}

class Forest {}
class EarthForest extends Forest {}
class MarsForest extends Forest {}

class TerrainFactory {
    private $sea;
    private $forest;
    private $plains;

    function __construct( Sea $sea, Plains $plains, Forest $forest ) {
        $this->sea = $sea;
        $this->plains = $plains;
        $this->forest = $forest;
    }

    function getSea( ) {
        return clone $this->sea;
    }

    function getPlains( ) {
        return clone $this->plains;
    }

    function getForest( ) {
        return clone $this->forest;
    }
}

$factory = new TerrainFactory( new EarthSea(),new EarthPlains(),new EarthForest() );
print_r( $factory->getSea() );
print_r( $factory->getPlains() );
print_r( $factory->getForest() );
?>

以上在初始化工厂的时候,传入了不同类的实例用于初始化工厂。当需要新的类型实例时,无须写一个新的创建者类,只需要简单地改变创建工厂时提供的参数,如: $factory = new TerrainFactory( new EarthSea(),new MarsPlains(),new MarsForest() );

同样是平行扩展,抽象工厂模式基于继承实现,原型模式基于组合实现。

组合模式

整体和局部可以互换,即容器对象和它们包含的对象共享相同的接口。比如一个士兵、一辆坦克有它们的战斗力值,一个由士兵、坦克组成的军队(一个集合)也有它的战斗力值。士兵、坦克、军队,它们都可以定义为单元,拥有共同的接口。

<?php
abstract class Unit {
    abstract function addUnit( Unit $unit );
    abstract function removeUnit( Unit $unit );
    abstract function bombardStrength();
}

class Army extends Unit {
    private $units = array();

    function addUnit( Unit $unit ) {
        if ( in_array( $unit, $this->units, true ) ) {
            return;
        }
        
        $this->units[] = $unit;
    }

    function removeUnit( Unit $unit ) {
        $this->units = array_udiff( $this->units, array( $unit ), 
                      function( $a, $b ) { return ($a === $b)?0:1; } );
    }

// 计算军队的战斗力
    function bombardStrength() {
        $ret = 0;
        foreach( $this->units as $unit ) {
            $ret += $unit->bombardStrength();
        }
        return $ret;
    }
}

class Tank extends Unit { 
function addUnit( Unit $unit ) {}				// 冗余方法,但是为了保证“透明性”必须存在
    function removeUnit( Unit $unit ) {}			// 冗余方法,但是为了保证“透明性”必须存在

    function bombardStrength() {
        return 4;
    }
}

class Soldier extends Unit { 
    function addUnit( Unit $unit ) {}				// 冗余方法,但是为了保证“透明性”必须存在
    function removeUnit( Unit $unit ) {}			// 冗余方法,但是为了保证“透明性”必须存在

    function bombardStrength() {
        return 8;
    }
}

$tank =  new Tank();
$tank2 = new Tank();
$soldier = new Soldier();

$army = new Army();
$army->addUnit( $soldier );
$army->addUnit( $tank );
$army->addUnit( $tank2 );

print_r( $army );

$army->removeUnit( $tank2 );

print_r( $army );
?>

装饰模式

功能定义完全依赖于继承体系会导致类的数量过多,而且代码会产生重复。 例如,定义一个Tile类表示一个作战区域,它有一个方法getWealthFactor()用于计算某个特定区域被一个玩家所占有后的收益:

<?php
abstract class Tile {
    abstract function getWealthFactor();
}

// 平原
class Plains extends Tile {
    private $wealthfactor = 2;
    function getWealthFactor() {
        return $this->wealthfactor;
    }
}

// 带钻石的平原
class DiamondPlains extends Plains {
    function getWealthFactor() {
        return parent::getWealthFactor() + 2;
    }
}

// 被污染的平原
class PollutedPlains extends Plains {
    function getWealthFactor() {
        return parent::getWealthFactor() - 4;
    }
}

$tile = new PollutedPlains();
print $tile->getWealthFactor();
?>

此时,如果想要获得一个既带钻石,又被污染的平原,如果基于集成来扩展功能,就只能创建一个形如PollutedDiamondPlains的新类。

装饰模式使用组合和委托,而不是只使用继承来解决功能变化问题。

<?php

abstract class Tile {
    abstract function getWealthFactor();
}

class Plains extends Tile {
    private $wealthfactor = 2;
    function getWealthFactor() {
        return $this->wealthfactor;
    }
}

// 引入一个委托类
// 委托类继承自Tile,因此与Tile的对象有相同的操作接口
// 委托类还包含一个指向Tile的引用,用于实现链式操作,在运行时轻松合并对象
abstract class TileDecorator extends Tile {
    protected $tile;
    function __construct( Tile $tile ) {
        $this->tile = $tile;
    }
}

class DiamondDecorator extends TileDecorator {
    // 在装饰器类中扩展功能
function getWealthFactor() {
        return $this->tile->getWealthFactor()+2;
    }
}

class PollutionDecorator extends TileDecorator {
    function getWealthFactor() {
        return $this->tile->getWealthFactor()-4;
    }
}

$tile = new Plains();
print $tile->getWealthFactor(); 			// 2

$tile = new DiamondDecorator( new Plains() );
print $tile->getWealthFactor();			 // 4

$tile = new PollutionDecorator( new DiamondDecorator( new Plains() ) );
print $tile->getWealthFactor(); 			// 0
?>

外观模式

外观模式是一个十分简单的概念,它只是为一个分层或者一个子系统创建一个单一的入口,方便客户端代码的使用,避免客户端代码使用子系统复杂的内部方法。

class ProductFacade {
    private $products = array();

    function __construct( $file ) {
        $this->file = $file;
        $this->compile();
    }

    private function compile() {
        // 复杂的操作
    }

    function getProducts() {
        return $this->products;
    }

    function getProduct( $id ) {
        return $this->products[$id];
    }
}

$facade = new ProductFacade( 'test.txt' );
$object = $facade->getProduct( 234 );

解释器模式

构造一个可以用于创建脚本化应用的迷你语言解释器。 [略]

策略模式

策略模式定义了一系列的算法,并将每一个算法封装起来,而且使它们还可以相互替换。策略模式让算法独立于使用它的客户而独立变化。 例如,有一个测验问题类Question,还有一个mark()方法用来表示回答。当用户回答问题时可以为答案选择不同的标记方式,因此继承Question类来实现针对不同回答方式的子类,如MarkLogicQuestion、MatchQuestion、RegexpQuestion。此时,如果新增需求,要求支持不同类型的问题,如TextQuestion、AVQuestion,而每种问题又同样要支持之前的3种标记语言,如果完全依赖继承,则需要创建6个类。 “组合优于继承”,只要发现正在不断地在继承树的各个分支中重复同一个算法,无论是通过子类还是通过重复条件语句,应该将这些算法抽象成独立的类型。该例中的“算法”,即mark()方法。

<?php
abstract class Question {
    protected $prompt;		// 问题提示
    protected $marker;		// 答案标记对象

    function __construct( $prompt, Marker $marker ) {
        $this->prompt=$prompt;
        $this->marker=$marker;
    }

    function mark( $response ) {		// 算法:标记用户的回答
        return $this->marker->mark( $response );
    }
}

class TextQuestion extends Question {
    // 文本类型的问题
}

class AVQuestion extends Question {
    // AV类型的问题
}

// 答案标记处理类
abstract class Marker {
    protected $test;

    function __construct( $test ) {
        $this->test = $test;
    }

    abstract function mark( $response );
}

class MarkLogicMarker extends Marker {		// mark()算法实现1
    function mark( $response ) {
        ...
return true;
    }
}

class MatchMarker extends Marker {			// mark()算法实现2
    function mark( $response ) {
        return ( $this->test == $response );
    }
}

class RegexpMarker extends Marker {			// mark()算法实现3
    function mark( $response ) {
        return ( preg_match( "$this->test", $response ) );
    }
}

// 使用
// 构造3个策略对象
$markers = array(	
new RegexpMarker( "/f.ve/" ),
 			new MatchMarker( "five" ),
    		new MarkLogicMarker( '$input equals "five"' )
		);

foreach ( $markers as $marker ) {
    print get_class( $marker )."\n";

// 用策略对象来处理问题
    $question = new TextQuestion( "how many beans make five", $marker );
    foreach ( array( "five", "four" ) as $response ) { 
        print "\tresponse: $response: ";
        if ( $question->mark( $response ) ) {
            print "well done\n";
        } else {
            print "never mind\n";
        }
    }
}

?>

观察者模式

观察者模式的核心是把客户元素(观察者)从一个中心类(主体,被观察者)中分离出来。当主体知道事件发生时,观察者需要被通知到,同时不能将主体与观察者之间的关系进行硬编码。 为达到以上目的,需要强制主体实现Observable接口,强制观察者实现Observer接口,并允许观察者在主体上进行注册。

<?php

// 被观察的主体需要实现该接口
interface Observable {
    function attach( Observer $observer );		// 注册观察者
    function detach( Observer $observer );		// 删除观察者
    function notify();						// 通知变化
}

// 观察者需要实现该接口
interface Observer {
    function update( Observable $observable );	// 当关注的事件发生时,触发的方法
}

class Login implements Observable {
    private $observers;						// 观察者列表
    private $storage;
    const LOGIN_USER_UNKNOWN = 1;
    const LOGIN_WRONG_PASS    = 2;
    const LOGIN_ACCESS          = 3;

    function __construct() {
        $this->observers = array();
    }

    function attach( Observer $observer ) {
        $this->observers[] = $observer;
    }

    function detach( Observer $observer ) {
        $this->observers = array_udiff( $this->observers, array( $observer ), 
                        function( $a, $b ) { return ($a === $b)?0:1; } );
    }

// 当事件发生时,逐一通知观察者
    function notify() {
        foreach ( $this->observers as $obs ) {
            $obs->update( $this );
        }
    }

    function handleLogin( $user, $pass, $ip ) {
        switch ( rand(1,3) ) {
            case 1: 
                $this->setStatus( self::LOGIN_ACCESS, $user, $ip );
                $ret = true; break;
            case 2:
                $this->setStatus( self::LOGIN_WRONG_PASS, $user, $ip );
                $ret = false; break;
            case 3:
                $this->setStatus( self::LOGIN_USER_UNKNOWN, $user, $ip );
                $ret = false; break;
        }
        $this->notify();		// 执行事件通知
        return $ret;
    }

    private function setStatus( $status, $user, $ip ) {
        $this->status = array( $status, $user, $ip ); 
    }

    function getStatus() {
        return $this->status;
    }
}

// 所有需要关注Login事件的类需要继承此类
abstract class LoginObserver implements Observer {
    private $login;		
    function __construct( Login $login ) {
        $this->login = $login; 
        $login->attach( $this );
    }

    function update( Observable $observable) {
// 判断使用的是自己被注册的Login对象,而不是任意的Observable对象
        if ( $observable === $this->login ) {
            $this->doUpdate( $observable );
        }
    }

    abstract function doUpdate( Login $login );
} 

// Login事件发生后需要执行的逻辑1
class SecurityMonitor extends LoginObserver {
    function doUpdate( Login $login ) {
        $status = $login->getStatus(); 
        if ( $status[0] == Login::LOGIN_WRONG_PASS ) {
            // send mail to sysadmin 
            print __CLASS__.":\tsending mail to sysadmin\n"; 
        }
    }
}

// Login事件发生后需要执行的逻辑2
class GeneralLogger  extends LoginObserver {
    function doUpdate( Login $login ) {
        $status = $login->getStatus(); 
        // add login data to log
        print __CLASS__.":\tadd login data to log\n"; 
    }
}

// Login事件发生后需要执行的逻辑3
class PartnershipTool extends LoginObserver {
    function doUpdate( Login $login ) {
        $status = $login->getStatus(); 
        // check $ip address 
        // set cookie if it matches a list
        print __CLASS__.":\tset cookie if it matches a list\n"; 
    }
}

// 使用
$login = new Login();
new SecurityMonitor( $login );  	// 注册1
new GeneralLogger( $login );		// 注册2
$pt = new PartnershipTool( $login );
$login->detach( $pt );
for ( $x=0; $x<10; $x++ ) {
    $login->handleLogin( "bob","mypass", '158.152.55.35' );
    print "\n";
}

?>

访问者模式

访问者表示一个作用于某对象结构中的各元素的操作。它使得可以在不改变各元素类的前提下定义作用于这些元素的新操作。把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。适用于数据结构相对稳定算法又易变化的系统。 如下在Unit类族的基础上添加新的功能。

<?php
/* 
应用组合模式实现的Unit类族
 */
abstract class Unit {
    protected $depth = 0;						// 节点深度

    function getComposite() {
        return null;
    }
    
    protected function setDepth( $depth ) {
        $this->depth=$depth;
    }

    function getDepth() {
        return $this->depth;
    }

    abstract function bombardStrength();			// 组合模式的统一接口

    function accept( ArmyVisitor $visitor ) {		// 接收一个访问者对象
        $method = "visit".get_class( $this );		// 拼凑访问者对象的方法,如当前对象是一个名为Archer的Unit的子类的对象,则拼凑出来的方法名为:visitArcher()
        $visitor->$method( $this );
    }
}

// 一种单节点单元
class Archer extends Unit {
    function bombardStrength() {
        return 4;
    }

}

// 一种单节点单元
class Cavalry extends Unit {
    function bombardStrength() {
        return 2;
    }
}

// 一种单节点单元
class LaserCanonUnit extends Unit {
    function bombardStrength() {
        return 44;
    }
}

// 多节点单元的基类
abstract class CompositeUnit extends Unit {
    private $units = array();

    function getComposite() {
        return $this;
    }

    function units() {
        return $this->units;
    }

    function removeUnit( Unit $unit ) {
        $units = array();
        foreach ( $this->units as $thisunit ) {
            if ( $unit !== $thisunit ) {
                $units[] = $thisunit;
            }
        }
        $this->units = $units;
    }

// 多节点对象在接受访问者对象时行为不同于单节点对象
    function accept( ArmyVisitor $visitor ) {
        parent::accept( $visitor );
        foreach ( $this->units as $thisunit ) {  // 所有子节点也应该可以被该访问者对象所访问
            $thisunit->accept( $visitor );
        }
    }

    function addUnit( Unit $unit ) {
        foreach ( $this->units as $thisunit ) {
            if ( $unit === $thisunit ) {
                return;
            }
        }
        $unit->setDepth($this->depth+1);
        $this->units[] = $unit;
    }
}

// 一种多节点单元
class TroopCarrier extends CompositeUnit {
    function addUnit( Unit $unit ) {
        if ( $unit instanceof Cavalry ) {
            throw new UnitException("Can't get a horse on the vehicle");
        }
        parent::addUnit( $unit );
    }

    function bombardStrength() {
        return 0;
    }
}

// 一种多节点单元
class Army extends CompositeUnit {

    function bombardStrength() {
        $ret = 0;
        foreach( $this->units() as $unit ) {
            $ret += $unit->bombardStrength();
        }
        return $ret;
    }
}

// ========================================================================
// 访问者的基类
abstract class ArmyVisitor  {
    abstract function visit( Unit $node );

/*
定义访问每种可访问对象的接口
实际都是重定向到visit(...)方法
*/
    function visitArcher( Archer $node ) {
        $this->visit( $node );
    }
    function visitCavalry( Cavalry $node ) {
        $this->visit( $node );
    }

    function visitLaserCanonUnit( LaserCanonUnit $node ) {
        $this->visit( $node );
    }

    function visitTroopCarrierUnit( TroopCarrierUnit $node ) {
        $this->visit( $node );
    }

    function visitArmy( Army $node ) {
        $this->visit( $node );
    }
}

// 每一个访问者子类即相当于提供了一组新的功能
// 一种访问者
class TextDumpArmyVisitor extends ArmyVisitor {
    private $text="";
// 重写visit方法定义特定功能
    function visit( Unit $node ) {
        $ret = "";
        $pad = 4*$node->getDepth();			// 使用了节点的方法
        $ret .= sprintf( "%{$pad}s", "" );
        $ret .= get_class($node).": ";
        $ret .= "bombard: ".$node->bombardStrength()."\n";
        $this->text .= $ret;
    }

// 该访问器的对外接口:触发访问的入口
    function getText() {
        return $this->text;
    }
}

// 一种访问者
class TaxCollectionVisitor extends ArmyVisitor {
    private $due=0;
    private $report="";

    function visit( Unit $node ) {
        $this->levy( $node, 1 );
    }

    function visitArcher( Archer $node ) {
        $this->levy( $node, 2 );
    }

    function visitCavalry( Cavalry $node ) {
        $this->levy( $node, 3 );
    }

    function visitTroopCarrierUnit( TroopCarrierUnit $node ) {
        $this->levy( $node, 5 );
    }

    private function levy( Unit $unit, $amount ) {
        $this->report .= "Tax levied for ".get_class( $unit );
        $this->report .= ": $amount\n";
        $this->due += $amount;
    }

// 该访问器的对外接口:触发访问的入口1
    function getReport() {
        return $this->report;
    }

// 该访问器的对外接口:触发访问的入口2
    function getTax() {
        return $this->due;
    }
}

// 使用
// 构建一个军队(一组节点,一棵树)
$main_army = new Army();
$main_army->addUnit( new Archer() );
$main_army->addUnit( new LaserCanonUnit() );
$main_army->addUnit( new Cavalry() );

$textdump = new TextDumpArmyVisitor();			// 实例化一个访问器
$main_army->accept( $textdump  );				// 节点数接受这个访问者对象
print $textdump->getText();						// 访问!

$taxcollector = new TaxCollectionVisitor();
$main_army->accept( $taxcollector );
print $taxcollector->getReport();
print "TOTAL: ";
print $taxcollector->getTax()."\n";
?>

命令模式

以对象来代表实际行动。命令对象可以把行动(action)及其参数封装起来。

<?php

class CommandNotFoundException extends Exception {}

// 通过CommandContext机制,请求数据可被传递给Command对象,同时响应也可以被返回到视图层 
class CommandContext {
    private $params = array();
    private $error = "";

    function __construct() {
        $this->params = $_REQUEST;		// 将请求参数映射到参数列表,请求参数中包含命令名称以及其他的参数
    }

    function addParam( $key, $val ) { 
        $this->params[$key]=$val;
    }

    function get( $key ) { 
        return $this->params[$key];
    }

    function setError( $error ) {
        $this->error = $error;
    }

    function getError() {
        return $this->error;
    }
}

// 生成命令对象的工厂
// 在commnds目录里查找特定的类文件
// 如果文件和类都存在,则返回命令对象给调用者
class CommandFactory {
    private static $dir = 'commands';
    static function getCommand( $action='Default' ) {
        if ( preg_match( '/\W/', $action ) ) {
            throw new Exception("illegal characters in action");
        }
        $class = UCFirst(strtolower($action))."Command";  
        $file = self::$dir.DIRECTORY_SEPARATOR."$class.php";
        if ( ! file_exists( $file ) ) {
            throw new CommandNotFoundException( "could not find '$file'" );
        }
        require_once( $file );
        if ( ! class_exists( $class ) ) {
            throw new CommandNotFoundException( "no '$class' class located" );
        }
        $cmd = new $class();
        return $cmd;
    }
}

// 命令调用者
class Controller {
    private $context;
    function __construct() {
        $this->context = new CommandContext();
    }

    function getContext() {
        return $this->context;
    }

    function process() {
// 在一个Web项目中,选择实例化哪个命令对象的最简单的办法是根据请求本身的参数来决定
        $cmd = CommandFactory::getCommand( $this->context->get('action') );
        if ( ! $cmd->execute( $this->context ) ) {
            // 处理错误
        } else {
            // 成功,返回视图
        }
    } 
}    

// 使用
$controller = new Controller();

// 获取controller的CommandContext,并设置参数
$context = $controller->getContext();	
$context->addParam('action', 'feedback' );  // 指定命令名称,将被用于查找类文件
$context->addParam('email', 'bob@bob.com' );
$context->addParam('topic', 'my brain' );
$context->addParam('msg', 'all about my brain' );

// 执行命令
$controller->process();
print $context->getError();

?>

命令类的具体实现:
	class FeedbackCommand extends Command{
function execute(CommandContext $context){
$email = $context->get(‘email’);
$msg = $context->get(‘msg’);
...
return true;
}
}

可以再定义其他的命令类,比如:
class LoginCommand extends Command{
function execute(CommandContext $context){
...
}
}

注册表

注册表是跳出层约束的主要途径之一,大多数模式只能用在某个层,但注册表是一个例外。 注册表的作用是提供系统级别的对象访问能力(跨层)。

<?php
namespace woo\base;

// 注册表基类,需要支持“应用程序作用域”的数据可以继承该类
abstract class Registry {
    abstract protected function get( $key );
    abstract protected function set( $key, $val );
}

// 请求-提供应用程序级别的、对请求对象的访问接口
class RequestRegistry extends Registry {
    ...
}

// 会话-提供应用程序级别的、对会话对象的访问接口
class SessionRegistry extends Registry {
    private static $instance;
    private function __construct() {
        session_start();
    }

    static function instance() {
        if ( ! isset(self::$instance) ) { self::$instance = new self(); }
        return self::$instance;
    }

    protected function get( $key ) {
        if ( isset( $_SESSION[__CLASS__][$key] ) ) {
            return $_SESSION[__CLASS__][$key];
        }
        return null;
    }

    protected function set( $key, $val ) {
        $_SESSION[__CLASS__][$key] = $val;
    }

    function setComplex( Complex $complex ) {
        self::instance()->set('complex', $complex);
    }

    function getComplex( ) {
        return self::instance()->get('complex');
    }
}

// 应用程序-代表应用程序自身
class ApplicationRegistry extends Registry {
    ...
}

class MemApplicationRegistry extends Registry {
    ...
}

class AppException extends \Exception {}
?>

// 使用
if ( ! isset( $argv[1] ) ) {
    // run script without argument to monitor
    while ( 1 ) {
        sleep(5);
        $thing = \woo\base\ApplicationRegistry::getDSN();

// 初始化各种注册表
        \woo\base\RequestRegistry::instance();
        \woo\base\SessionRegistry::instance();
        \woo\base\MemApplicationRegistry::instance();

        print "dsn is {$thing}\n";
    }
} else {
    // run script with argument in separate window to change value.. see the result in monitor process
    print "setting dsn {$argv[1]}\n"; 
    \woo\base\ApplicationRegistry::setDSN($argv[1]);
}

前端控制器

用一个中心来处理所有到来的请求:单一入口。 所有请求都定向到index.php中,该文件内容如下: require(“woo/controller/Controller.php”); \woo\controller\Controller::run(); // 创建并运行一个前端控制器来处理所有的操作 前端控制器委托ApplicationHelper对象来初始化执行环境,然后从CommandResolver对象获取一个Command对象,最后调用Command::execute()处理业务逻辑。

<?php
namespace woo\controller;

// 前端控制器类
class Controller {
    private $applicationHelper;				// 辅助类,用于初始化环境变量

    private function __construct() {}

// 控制器执行入口
    static function run() {
        $instance = new Controller();	
        $instance->init();					// 初始化
        $instance->handleRequest();			// 处理请求
    }

    function init() {
        $applicationHelper = ApplicationHelper::instance();
        $applicationHelper->init();
    }

// 实际处理请求的地方
    function handleRequest() {
        $request = new \woo\controller\Request();				// 请求对象
        $cmd_r = new \woo\command\CommandResolver();		// 命令解析对象
        $cmd = $cmd_r->getCommand( $request );				// 获取命令对象
        $cmd->execute( $request );							// 处理请求
    }
}

// 辅助类,用于初始化环境变量。是一个单例,并用注册表模式操作应用程序级对象
class ApplicationHelper {
    private static $instance;
    private $config = "/tmp/data/woo_options.xml";				// 配置文件路径

    private function __construct() {}

    static function instance() {
        if ( ! self::$instance ) {
            self::$instance = new self();
        }
        return self::$instance;
    }

    function init() {
        $dsn = \woo\base\ApplicationRegistry::getDSN( );
        if ( ! is_null( $dsn ) ) {
            return;
        }
        $this->getOptions();
     }

     private function getOptions() {
        $this->ensure( file_exists( $this->config  ),"Could not find options file" );
        $options = SimpleXml_load_file( $this->config );
        print get_class( $options );
        $dsn = (string)$options->dsn;
        $this->ensure( $dsn, "No DSN found" );
        \woo\base\ApplicationRegistry::setDSN( $dsn );
        // set other values
    }

    private function ensure( $expr, $message ) {
        if ( ! $expr ) {
            throw new \woo\base\AppException( $message );
        }
    }
}

// 请求类
class Request {
    private $properties;
    private $feedback = array();

    function __construct() {
        $this->init();
        \woo\base\RequestRegistry::setRequest($this );			// 设置应用程序注册表中的请求对象为自己
    }

    function init() {
        if ( isset( $_SERVER['REQUEST_METHOD'] ) ) {
            $this->properties = $_REQUEST;
            return;
        }
        foreach( $_SERVER['argv'] as $arg ) {
            if ( strpos( $arg, '=' ) ) {
                list( $key, $val )=explode( "=", $arg );
                $this->setProperty( $key, $val );
            }
        }
    }

    function getProperty( $key ) {
        if ( isset( $this->properties[$key] ) ) {
            return $this->properties[$key];
        }
    }

    function setProperty( $key, $val ) {
        $this->properties[$key] = $val;
    }
    
    function addFeedback( $msg ) {
        array_push( $this->feedback, $msg );
    }
 
    function getFeedback( ) {
        return $this->feedback;
    }

    function getFeedbackString( $separator="\n" ) {
        return implode( $separator, $this->feedback );
    }
}



namespace woo\command;
// 命令对象的基类。前端控制器的主要功能就是根据请求参数映射到不同的命令
abstract class Command {
    final function __construct() { }

// 执行命令
    function execute( \woo\controller\Request $request ) {
        $this->doExecute( $request );
    }

    abstract function doExecute( \woo\controller\Request $request );
}

// 默认的命令
class DefaultCommand extends Command {
    function doExecute( \woo\controller\Request $request ) {
        $request->addFeedback( "Welcome to WOO" );
        include( "woo/view/main.php");					// 分配视图
    }
}

// 命令解析器(生成不同命令的工厂)
class CommandResolver {
    private static $base_cmd;
    private static $default_cmd;

    function __construct() {
        if ( ! self::$base_cmd ) {
            self::$base_cmd = new \ReflectionClass( "\woo\command\Command" );		// 使用反射
            self::$default_cmd = new DefaultCommand();
        }
    }

// 获取命令对象
    function getCommand( \woo\controller\Request $request ) {
        $cmd = $request->getProperty( 'cmd' );
        $sep = DIRECTORY_SEPARATOR;
        if ( ! $cmd ) {
            return self::$default_cmd;
        }
        $cmd=str_replace( array('.', $sep), "", $cmd );
        $filepath = "woo{$sep}command{$sep}{$cmd}.php";
        $classname = "woo\\command\\{$cmd}";
        if ( file_exists( $filepath ) ) {
            @require_once( "$filepath" );
            if ( class_exists( $classname) ) {
                $cmd_class = new ReflectionClass($classname);// 使用反射的方式,根据类名返回类实例
                if ( $cmd_class->isSubClassOf( self::$base_cmd ) ) {
                    return $cmd_class->newInstance();
                } else {
                    $request->addFeedback( "command '$cmd' is not a Command" );
                }
            }
        }
        $request->addFeedback( "command '$cmd' not found" );
        return clone self::$default_cmd;
    }
}

?>

应用控制器*

应用控制器负责映射请求到命令,并映射命令到视图(通过一个XML配置文件)。 比前端控制器复杂的地方在于进一步对Command进行分离(前端控制器中视图与Command未分离)。

页面控制器

比前端控制器更简单的实现:控制逻辑被放在视图里面。 最简单的页面控制器代码如下:

 	<?php
require_once("woo/domain/Venue.php");
try {
    $venues = \woo\domain\Venue::findAll();
} catch ( Exception $e ) {
    include( 'error.php' );
    exit(0);
}
// default page follows
?>
<html>
<head>
<title>Venues</title>
</head>
<body>
<h1>Venues</h1>

<?php foreach( $venues as $venue ) { ?>
    <?php print $venue->getName(); ?><br />
<?php } ?>

</body>
</html>

模板视图和视图助手 不应该在视图中混入业务逻辑代码,应该把应用处理逻辑放在视图外的地方,只允许视图执行“显示数据”的功能,通常的做法是先得到数据,再将数据传递给视图。当一个视图需要访问系统数据时,可以提供一个视图助手对象来帮助视图达到目的:

<?php
namespace woo\view;

// 定义一个视图助手类
class VH {
    static function getRequest() {
        return \woo\base\RequestRegistry::getRequest();
    }
}

?>

在视图中使用视图助手类:

require_once("woo/view/ViewHelper.php");
$request = \woo\view\VH::getRequest();		// 通过视图助手类来获得请求对象
$venue = $request->getObject('venue');			// 获得视图所需的数据
?>

<html>
<head>
<title>Add a Space for venue <?php echo $venue->getName() ?></title>
</head>
<body>
<h1>Add a Space for Venue '<?php print $venue->getName() ?>'</h1>

<table>
<tr>
<td>
<?php print $request->getFeedbackString("</td></tr><tr><td>"); ?>
</td>
</tr>
</table>

<form method="post">
    <input type="text"
     value="<?php echo $request->getProperty( 'space_name' ) ?>" name="space_name"/>
    <input type="hidden" name="venue_id" value="<?php echo $venue->getId() ?>" />
    <input type="submit" value="submit" />
</form>

</body>
</html>

事务脚本

事务即一个功能,事务脚本提供一个快速而有效的机制来满足系统目标,自己处理请求(不分层,比如把连接数据库和计算业务逻辑的代码耦合在一个类中),而不是委托给特定的对象来完成。好处在于可以很快就得到想要的结果,但一般只应用在小型项目中。

<?php
namespace woo\process;

// 封装不同事务的公共操作
abstract class Base {
    static $DB;
    static $stmts = array();
   
    function __construct() {
        $dsn = \woo\base\ApplicationRegistry::getDSN( );
        if ( is_null( $dsn ) ) {
            throw new \woo\base\AppException( "No DSN" );
        }

        self::$DB = new \PDO( $dsn );
        self::$DB->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
    } 

    function prepareStatement( $stmt_s ) {					// 拼装SQL语句
        if ( isset( self::$stmts[$stmt_s] ) ) {
            return self::$stmts[$stmt_s];
        }
        $stmt_handle = self::$DB->prepare($stmt_s);
        self::$stmts[$stmt_s]=$stmt_handle;
        return $stmt_handle;
    } 

    protected function doStatement( $stmt_s, $values_a ) {	// 执行SQL查询
        $sth = $this->prepareStatement( $stmt_s );
        $sth->closeCursor();
        $db_result = $sth->execute( $values_a );
        return $sth;
    }
}

class VenueManager extends Base {
    static $add_venue =  "INSERT INTO venue ( name ) values( ? )";
    static $add_space  = "INSERT INTO space( name, venue ) values( ?, ? )"; 
    static $check_slot = "SELECT id, name FROM event WHERE space = ? AND (start+duration) > ? AND start < ?"; 
    static $add_event =  "INSERT INTO event ( name, space, start, duration ) values( ?, ?, ?, ? )"; 

// 一个事务:添加空间
    function addVenue( $name, $space_array ) {
        $ret = array();
        $ret['venue'] = array( $name ); 
        $this->doStatement( self::$add_venue, $ret['venue']);
        $v_id = self::$DB->lastInsertId();
        $ret['spaces'] = array();
        foreach ( $space_array as $space_name ) {
            $values = array( $space_name, $v_id );
            $this->doStatement( self::$add_space, $values);
            $s_id = self::$DB->lastInsertId();
            array_unshift( $values, $s_id );
            $ret['spaces'][] = $values;
        }
        return $ret;
    }
    
// 一个事务:注册事件
    function bookEvent( $space_id, $name, $time, $duration ) {
        $values = array( $space_id, $time, ($time+$duration) ); 
        $stmt = $this->doStatement( self::$check_slot, $values, false ) ;
        if ( $result = $stmt->fetch() ) {
            throw new \woo\base\AppException( "double booked! try again" );
        }
        $this->doStatement( self::$add_event, 
            array( $name, $space_id, $time, $duration ) );
    } 

// 还可以用同样的形式定义其他的基于数据库操作的事务,快速实现功能。
}
$halfhour = (60*30);
$hour     = (60*60);
$day      = (24*$hour);

$mode="sqlite";
if ( $mode == 'mysql' ) {
    $dsn = "mysql:dbname=test";    
} else {
    $dsn = "sqlite://tmp/data/woo.db";    
}

\woo\base\ApplicationRegistry::setDSN( $dsn ); 
$mgr = new VenueManager();
$ret = $mgr->addVenue( "The Eyeball Inn", array( 'The Room Upstairs', 'Main Bar' ));
$space_id = $ret['spaces'][0][0];
$mgr->bookEvent( $space_id, "Running like the rain", time()+($day), ($hour-5) ); 
$mgr->bookEvent( $space_id, "Running like the trees", time()+($day-$hour), (60*60) ); 
?>

领域模型

领域模型象征着真实世界里项目中的各个参与者,是更纯粹的“万物皆对象”,在其他地方对象总是带着某种具体的责任,而在领域模型中,它们常常被描述为一组属性及附加的代理。常见的做法是将领域模型类直接映射为关系数据库的表。

<?php
namespace woo\domain;

// 抽象基类
abstract class DomainObject {
    private $id;

    function __construct( $id=null ) {
        $this->id = $id;
    }

    function getId( ) {
        return $this->id;
    }

    static function getCollection( $type ) {
        return array();			 		// dummy
    }
 
    function collection() {
        return self::getCollection( get_class( $this ) );
    }
}

// 对应数据库中的Venue表
class Venue extends DomainObject {
    private $name;		// 字段1
    private $spaces;	// 字段2

    function __construct( $id=null, $name=null ) {
        $this->name = $name;
        $this->spaces = self::getCollection("\\woo\\domain\\Space");
        parent::__construct( $id );
    }

    function setSpaces( SpaceCollection $spaces ) {
        $this->spaces = $spaces;
    }

    function getSpaces() {
        return $this->spaces;
    }

    function addSpace( Space $space ) {
        $this->spaces->add( $space );
        $space->setVenue( $this );
    }

    function setName( $name_s ) {
        $this->name = $name_s;
        $this->markDirty();
    }

    function getName( ) {
        return $this->name;
    }
}

$v = new Venue( null, "The grep and grouch" );
?>

数据映射器

数据映射器是一个负责将数据库数据映射到对象的类。 通常习惯为每一个领域类实现一个映射类。

// 数据映射器的基类
abstract class Mapper {
    protected static $PDO; 
    function __construct() {
        if ( ! isset(self::$PDO) ) { 
            $dsn = \woo\base\ApplicationRegistry::getDSN( );
            if ( is_null( $dsn ) ) {
                throw new \woo\base\AppException( "No DSN" );
            }
            self::$PDO = new \PDO( $dsn );
            self::$PDO->setAttribute(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION);
        }
    }

// 根据ID从数据库中查找记录,并映射为一个对象返回
    function find( $id ) {
        $this->selectstmt()->execute( array( $id ) );
        $array = $this->selectstmt()->fetch( ); 
        $this->selectstmt()->closeCursor( ); 
        if ( ! is_array( $array ) ) { return null; }
        if ( ! isset( $array['id'] ) ) { return null; }
        $object = $this->createObject( $array );			// 重定向到createObject(...)
        return $object; 
    }

// 根据一个数组参数创建一个对象返回
    function createObject( $array ) {
        $obj = $this->doCreateObject( $array );			// 重定向到doCreateObject(...)
        return $obj;
    }

    function insert( \woo\domain\DomainObject $obj ) {	// 重定向到doInsert(...)
        $this->doInsert( $obj );
    }

// 留待子类实现的方法
    abstract function update( \woo\domain\DomainObject $object );
    protected abstract function doCreateObject( array $array );
    protected abstract function doInsert( \woo\domain\DomainObject $object );
    protected abstract function selectStmt();
}

// 对应数据库中Venue表的数据映射器
class VenueMapper extends Mapper {
    function __construct() {
        parent::__construct();
// 在构造方法中预定义了一些sql语句模板,其中就关联了指定的表venue
        $this->selectStmt = self::$PDO->prepare( "SELECT * FROM venue WHERE id=?");
        $this->updateStmt = self::$PDO->prepare("update venue set name=?, id=? where id=?");
        $this->insertStmt = self::$PDO->prepare("insert into venue ( name ) values( ? )");
    } 
    
// 在对象关系层面,一个venue可以包含多个space
    function getCollection( array $raw ) {
        return new SpaceCollection( $raw, $this );
    }

// 创建一个新对象,此时与数据库无关
    protected function doCreateObject( array $array ) {
        $obj = new \woo\domain\Venue( $array['id'] );
        $obj->setname( $array['name'] );
        // $space_mapper = new spacemapper();
        // $space_collection = $space_mapper->findbyvenue( $array['id'] );
        // $obj->setspaces( $space_collection );
        return $obj;
    }

// 插入:将对象映射到数据库
    protected function doInsert( \woo\domain\DomainObject $object ) {
        print "inserting\n";
        debug_print_backtrace();
        $values = array( $object->getName() ); 
        $this->insertStmt->execute( $values );
        $id = self::$PDO->lastInsertId();
        $object->setId( $id );
    }
    
// 更新
    function update( \woo\domain\DomainObject $object ) {
        print "updating\n";
        $values = array( $object->getName(), $object->getId(), $object->getId() ); 
        $this->updateStmt->execute( $values );
    }

    function selectStmt() {
        return $this->selectStmt;
    }
}

// 使用
$mapper = new VenueMapper();
$venue = $mapper->find(2);				// 使用数据映射器来填充一个领域对象模型
print_r( $venue );

标识映射

保存每个已加载过的对象,确保每个对象只加载一次(相当于在数据映射器与数据库之间又加了一层逻辑)当要访问他们的时候,通过映射来查找它们。标识映射的主要目的是保持一致性,而不是提高性能。: (1)当要访问对象时,首先检查标识映射,看需要的对象是否已经存在其中。 (2)使用Identify来确保不重复加载相同的数据,不仅有助于保证正确性(不会将同一数据加载到两个不同的对象上),还能提升性能。

// 一个标识映射类(一个对象池)
// 同时实现了标识映射模式和工作单元模式
class ObjectWatcher {
    private $all = array();			// 已加载的所有对象
    private $dirty = array();
    private $new = array();
    private $delete = array();
    private static $instance;

    private function __construct() { }

// 单例
    static function instance() {
        if ( ! self::$instance ) {
            self::$instance = new ObjectWatcher();
        }
        return self::$instance;
    }
 
// 构造唯一标识,确保不存在重复对象
    function globalKey( DomainObject $obj ) {
        $key = get_class( $obj ).".".$obj->getId();
        return $key;
    }
  
// 添加新的加载对象,如果已存在则覆盖
    static function add( DomainObject $obj ) {
        $inst = self::instance();
        $inst->all[$inst->globalKey( $obj )] = $obj;
    }

// 判断某个类的某个对象是否已被加载
    static function exists( $classname, $id ) {
        $inst = self::instance();
        $key = "$classname.$id";
        if ( isset( $inst->all[$key] ) ) {
            return $inst->all[$key];
        }
        return null;
    }
 
/***
以下是工作单元模式的实现
***/
    static function addDelete( DomainObject $obj ) {				// 删除
        $self = self::instance();
        $self->delete[$self->globalKey( $obj )] = $obj;
    }

    static function addDirty( DomainObject $obj ) {				// 添加脏对象
        $inst = self::instance();
        if ( ! in_array( $obj, $inst->new, true ) ) {
            $inst->dirty[$inst->globalKey( $obj )] = $obj;
        }
    }

    static function addNew( DomainObject $obj ) {				// 新对象
        $inst = self::instance();
        // we don't yet have an id
        $inst->new[] = $obj;
    }

    static function addClean(DomainObject $obj ) {				// 将脏对象列表中的某个对象标识为干净的
        $self = self::instance();
        unset( $self->delete[$self->globalKey( $obj )] );
        unset( $self->dirty[$self->globalKey( $obj )] );

        $self->new = array_filter( $self->new, 
                function( $a ) use ( $obj ) { return !( $a === $obj ); } 
                );
    }

    function performOperations() {							// 提交所有操作
        foreach ( $this->dirty as $key=>$obj ) {
            $obj->finder()->insert( $obj );
        }
        foreach ( $this->new as $key=>$obj ) {
            $obj->finder()->insert( $obj );
        }
        $this->dirty = array();
        $this->new = array();
    } 
}

工作单元

工作单元的目标是维护变化的对象列表。收集变化的对象,并将变化的对象放到各自的增删改列表中,最后Commit,Commit时需要循环遍历这些列表。

工作单元模式和标识映射模式是互补的,标识映射模式在读数据库之前加了一层逻辑,保证不加载重复的对象。工作单元模式在写数据之前加了一层逻辑,保证不会重复提交数据库操作。

脏对象:当对象从数据库中取出并被修改后,就认为该对象“脏”了,脏对象保存在$dirty数组中,直到更新数据库。可以将一个脏对象标识为干净的,这样数据库就不会被更新。

代码见上例。

延迟加载

一个对象,它虽然不包含所需要的所有数据,但是知道如何去获取这些数据。 加载一个对象会引起大量相关对象的加载,如初始化某对象的时候,其中的某些域(属性)需要去连接数据库(或者说连接其他数据库),也就是说某个对象的是从数据库A中获得,但其部分属性需要从其他数据库获得,这时候需要考虑采用延迟加载来减少初始化时连接数据库的次数。

[代码略]

领域对象工厂

把映射器类的createObject()方法移出来放到一个独立的类中,就构成了领域对象工厂模式。

// 领域对象工厂基类
abstract class DomainObjectFactory {
    abstract function createObject( array $array );

    protected function getFromMap( $class, $id ) {
        return \woo\domain\ObjectWatcher::exists( $class, $id );
    }

    protected function addToMap( \woo\domain\DomainObject $obj ) {
        return \woo\domain\ObjectWatcher::add( $obj );
    }

}

// Venue类的领域对象工厂类
class VenueObjectFactory extends DomainObjectFactory {
    function createObject( array $array ) {
        $class = '\woo\domain\Venue';
        $old = $this->getFromMap( $class, $array['id'] );
        if ( $old ) { return $old; }
        $obj = new $class( $array['id'] );
        $obj->setname( $array['name'] );
        //$space_mapper = new SpaceMapper();
        //$space_collection = $space_mapper->findByVenue( $array['id'] );
        //$obj->setSpaces( $space_collection );
        $this->addToMap( $obj );
        return $obj;
    }
}

// Space类的领域对象工厂类
class SpaceObjectFactory extends DomainObjectFactory {
    function createObject( array $array ) {
        $class = '\woo\domain\Space';
        $old = $this->getFromMap( $class, $array['id'] );
        if ( $old ) { return $old; }
        $obj = new $class( $array['id'] );
        $obj->setname( $array['name'] );
        $ven_mapper = new VenueMapper();
        $venue = $ven_mapper->find( $array['venue'] );
        $obj->setVenue( $venue );

        $event_mapper = new EventMapper();
        $event_collection = $event_mapper->findBySpaceId( $array['id'] );        
        $obj->setEvents( $event_collection );
        return $obj;
    }
}

抖音
文章目录