Author: Kris.Wallsmith
Date: 2010-02-20 02:11:48 +0100 (Sat, 20 Feb 2010)
New Revision: 28144

Added:
   plugins/sfDoctrineMasterSlavePlugin/trunk/
   plugins/sfDoctrineMasterSlavePlugin/trunk/LICENSE
   plugins/sfDoctrineMasterSlavePlugin/trunk/README
   plugins/sfDoctrineMasterSlavePlugin/trunk/config/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/config/sfDoctrineMasterSlavePluginConfiguration.class.php
   plugins/sfDoctrineMasterSlavePlugin/trunk/lib/
   plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/sfDoctrineMasterSlaveCollection.class.php
   plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveConnectionManager.class.php
   
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveDebugListener.class.php
   plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/sfDoctrineMasterSlaveQuery.class.php
   plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/sfDoctrineMasterSlaveRecord.class.php
   plugins/sfDoctrineMasterSlavePlugin/trunk/package.xml.tmpl
   plugins/sfDoctrineMasterSlavePlugin/trunk/test/
   plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/
   plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/
   
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveConnectionManagerTest.php
   
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveDebugListenerTest.php
Log:
[sfDoctrineMasterSlavePlugin] initial import

Added: plugins/sfDoctrineMasterSlavePlugin/trunk/LICENSE
===================================================================
--- plugins/sfDoctrineMasterSlavePlugin/trunk/LICENSE                           
(rev 0)
+++ plugins/sfDoctrineMasterSlavePlugin/trunk/LICENSE   2010-02-20 01:11:48 UTC 
(rev 28144)
@@ -0,0 +1,7 @@
+Copyright (c) Kris Wallsmith
+
+Permission is hereby granted, free of charge, to any person obtaining a copy 
of this software and associated documentation files (the "Software"), to deal 
in the Software without restriction, including without limitation the rights to 
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 
of the Software, and to permit persons to whom the Software is furnished to do 
so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all 
copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 
SOFTWARE.

Added: plugins/sfDoctrineMasterSlavePlugin/trunk/README
===================================================================
--- plugins/sfDoctrineMasterSlavePlugin/trunk/README                            
(rev 0)
+++ plugins/sfDoctrineMasterSlavePlugin/trunk/README    2010-02-20 01:11:48 UTC 
(rev 28144)
@@ -0,0 +1,153 @@
+The `sfDoctrineMasterSlavePlugin` plugin manages database connections and
+directs queries to the appropriate connection: either the master or a slave
+database.
+
+Once the plugin is enabled you can mark a connection as the master connection
+in `databases.yml` by using the string "master" in its name:
+
+    all:
+      master:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=database;host:master.example.com
+          username: root
+          password: ~
+      slave:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=database;host:slave.example.com
+          username: root
+          password: ~
+
+Alternatively, you can provide an `is_master` parameter in the configuration:
+
+    all:
+      doctrine:
+        class: sfDoctrineDatabase
+        param:
+          dsn:       mysql:dbname=database;host:master.example.com
+          username:  root
+          password:  ~
+          is_master: true
+
+If no connection is marked as the master connection using either of these
+techniques, the first connection configured first will be used. If multiple
+connections are marked as master, only the last connection marked will be used
+as the master connection; the others will be used as slaves.
+
+If your configuration includes more than one slave database, the plugin will
+select one of them at random to use for the duration of the request.
+
+Accessing the right connection
+------------------------------
+
+You can access the master or slave connections from the configuration object:
+
+    [php]
+    ProjectConfiguration::getActive()->getMasterConnection();
+    ProjectConfiguration::getActive()->getSlaveConnection();
+
+The plugin interacts with Doctrine as your database connection objects are
+created and updates the current connection set in the `Doctrine_Manager` to
+be the master connection. This way, any calls to
+`Doctrine_Manager::connection()` will return the master connection, which is
+usually what you need when calling that method (i.e. when beginning a
+transaction).
+
+>**NOTE**
+>The slave database accessor `->getSlaveConnection()` includes logic to check
+>whether the master database has any open transactions, and will return the
+>master connection in that case.
+
+Choosing from multiple slaves
+-----------------------------
+
+If you have multiple slave connections configured in `databases.yml`, the
+plugin will choose one at random to use for the duration of the current
+request. If you want to customize the logic surrounding how a slave connection
+is selected, you can do so by listening to the `doctrine.select_slave` event.
+
+    /** Listens to the doctrine.select_slave event. */
+    public function selectSlave(sfEvent $event)
+    {
+      if (in_array('slave1', $event['slaves]))
+      {
+        $event->setReturnValue('slave1');
+        return true;
+      }
+    }
+
+The `doctrine.select_slave` event includes the following parameters:
+
+  * `group`:  The group to select a slave connection from
+  * `master`: The name of the master connection
+  * `slaves`: An array of slave connection names
+
+Notice this event receives the names of connections as parameters, not the
+connection objects themselves. You can access the connection objects in your
+event listener by calling
+`Doctrine_Manager::getInstance()->getConnection($name)`.
+
+Connecting to multiple schemas
+------------------------------
+
+If your schema assigns different connections to different models, you will
+need to organize the master and slave connections for each of these schema
+into groups using the `group` parameter:
+
+    all:
+      # db1 connections
+      db1_master:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=db1;host:db1-master.example.com
+          username: root
+          password: ~
+          group:    db1
+      db1_slave:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=db1;host:db1-slave.example.com
+          username: root
+          password: ~
+          group:    db1
+
+      # db2 connections
+      db2_master:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=db2;host:db2-master.example.com
+          username: root
+          password: ~
+          group:    db2
+      db2_slave:
+        class: sfDoctrineDatabase
+        param:
+          dsn:      mysql:dbname=db2;host:db2-slave.example.com
+          username: root
+          password: ~
+          group:    db2
+
+Emulating read-only connections
+-------------------------------
+
+It's important to be able to test whether your connection management is
+working correctly even when you're not connecting to any read-only databases.
+The plugin provides a connection listener that emulates a read-only connection
+for this purpose. Setup distinct master and slave connections in
+`databases.yml` to enable this emulation in debug and test modes.
+
+    dev:
+      doctrine:
+        class: sfDoctrineDatabase
+        param:
+          dsn:       mysql:dbname=database;host=localhost
+          username:  root
+          password:  ~
+          is_master: true
+      slave:
+        class: sfDoctrineDatabase
+        param:
+          dsn:       mysql:dbname=database;host=localhost
+          username:  root
+          password:  ~

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/config/sfDoctrineMasterSlavePluginConfiguration.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/config/sfDoctrineMasterSlavePluginConfiguration.class.php
                         (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/config/sfDoctrineMasterSlavePluginConfiguration.class.php
 2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,138 @@
+<?php
+
+/**
+ * Plugin configuration.
+ * 
+ * Adds the following parameters to each Doctrine database:
+ * 
+ *  * is_master: Whether the database is a master connection
+ *  * group:     Groups a connection with others
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage config
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+class sfDoctrineMasterSlavePluginConfiguration extends sfPluginConfiguration
+{
+  protected
+    $connectionManager = null;
+
+  /**
+   * @see sfPluginConfiguration
+   */
+  public function configure()
+  {
+    $a = array_search('sfDoctrineMasterSlavePlugin', 
$this->configuration->getPlugins());
+    $b = array_search('sfDoctrinePlugin', $this->configuration->getPlugins());
+
+    if ($a > $b)
+    {
+      throw new LogicException('The sfDoctrineMasterSlavePlugin plugin must be 
enabled before sfDoctrinePlugin');
+    }
+  }
+
+  /**
+   * @see sfPluginConfiguration
+   */
+  public function initialize()
+  {
+    $this->connectionManager = new 
sfDoctrineMasterSlaveConnectionManager($this->dispatcher);
+
+    $this->dispatcher->connect('configuration.method_not_found', array($this, 
'listenForConfigurationMethodNotFound'));
+    $this->dispatcher->connect('doctrine.configure', array($this, 
'configureDoctrine'));
+    $this->dispatcher->connect('doctrine.configure_connection', array($this, 
'configureDoctrineConnection'));
+    $this->dispatcher->connect('doctrine.filter_model_builder_options', 
array($this, 'filterBuilderOptions'));
+  }
+
+  /**
+   * Returns the current connection manager.
+   * 
+   * @return sfDoctrineMasterSlaveConnectionManager
+   */
+  public function getConnectionManager()
+  {
+    return $this->connectionManager;
+  }
+
+  /**
+   * Listens for the configuration.method_not_found event.
+   * 
+   * Adds accessors for master and slave connections to the configuration 
object.
+   * 
+   * @param sfEvent $event A symfony event
+   * 
+   * @return boolean Returns true if the event was processed
+   */
+  public function listenForConfigurationMethodNotFound(sfEvent $event)
+  {
+    switch ($event['method'])
+    {
+      case 'getMasterConnection':
+      case 'getSlaveConnection':
+        
$event->setReturnValue(call_user_func_array(array($this->connectionManager, 
$event['method']), $event['arguments']));
+        return true;
+    }
+
+    return false;
+  }
+
+  /**
+   * Configures Doctrine.
+   * 
+   * Adds custom query and collection classes if none are setup already.
+   * 
+   * @param sfEvent $event A symfony event
+   */
+  public function configureDoctrine(sfEvent $event)
+  {
+    $manager = $event->getSubject();
+
+    if ('Doctrine_Query' == 
$manager->getAttribute(Doctrine_Core::ATTR_QUERY_CLASS))
+    {
+      $manager->setAttribute(Doctrine_Core::ATTR_QUERY_CLASS, 
'sfDoctrineMasterSlaveQuery');
+    }
+
+    if ('Doctrine_Collection' == 
$manager->getAttribute(Doctrine_Core::ATTR_COLLECTION_CLASS))
+    {
+      $manager->setAttribute(Doctrine_Core::ATTR_COLLECTION_CLASS, 
'sfDoctrineMasterSlaveCollection');
+    }
+  }
+
+  /**
+   * Configures a Doctrine connection.
+   * 
+   * Registers each connection with the current master/slave connection 
manager.
+   * 
+   * @param sfEvent $event A symfony event
+   */
+  public function configureDoctrineConnection(sfEvent $event)
+  {
+    $database = $event['database'];
+    $conn = $event['connection'];
+
+    $this->connectionManager->register($conn, 
$database->getParameter('group'), $database->getParameter('is_master'));
+
+    if (sfConfig::get('sf_debug') || sfConfig::get('sf_test'))
+    {
+      $callable = new sfCallable(array($this->connectionManager, 
'getMasterConnection'));
+      $conn->addListener(new sfDoctrineMasterSlaveDebugListener($callable), 
'slave_emulator');
+    }
+  }
+
+  /**
+   * Filters Doctrine builder options.
+   * 
+   * @param sfEvent $event   A symfony event
+   * @param array   $options An array of builder options
+   * 
+   * @return array The filtered array of builder options
+   */
+  public function filterBuilderOptions(sfEvent $event, $options)
+  {
+    if ('sfDoctrineRecord' == $options['baseClassName'])
+    {
+      $options['baseClassName'] = 'sfDoctrineMasterSlaveRecord';
+    }
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/config/sfDoctrineMasterSlavePluginConfiguration.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/sfDoctrineMasterSlaveCollection.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/sfDoctrineMasterSlaveCollection.class.php
                          (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/sfDoctrineMasterSlaveCollection.class.php
  2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Overrides certain methods that require a master connection.
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage collection
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+class sfDoctrineMasterSlaveCollection extends Doctrine_Collection
+{
+  /**
+   * Saves the current collection of records.
+   * 
+   * Forces a master connection.
+   * 
+   * @see Doctrine_Collection
+   */
+  public function save(Doctrine_Connection $conn = null, $processDiff = true)
+  {
+    $conn = ProjectConfiguration::getActive()->getMasterConnection($conn ? 
$conn : $this->getTable()->getConnection());
+
+    return parent::save($conn, $processDiff);
+  }
+
+  /**
+   * Deletes the current collection of records.
+   * 
+   * Forces a master connection.
+   * 
+   * @see Doctrine_Collection
+   */
+  public function delete(Doctrine_Connection $conn = null, $clearColl = true)
+  {
+    $conn = ProjectConfiguration::getActive()->getMasterConnection($conn ? 
$conn : $this->getTable()->getConnection());
+
+    return parent::delete($conn, $clearColl);
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/collection/sfDoctrineMasterSlaveCollection.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveConnectionManager.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveConnectionManager.class.php
                           (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveConnectionManager.class.php
   2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,222 @@
+<?php
+
+/**
+ * Manages Doctrine master and slave connections.
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage connection
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+class sfDoctrineMasterSlaveConnectionManager
+{
+  const
+    DEFAULT_GROUP = 'default';
+
+  protected
+    $dispatcher  = null,
+    $connections = array();
+
+  /**
+   * Constructor.
+   * 
+   * @param sfEventDispatcher $dispatcher The event dispatcher
+   */
+  public function __construct(sfEventDispatcher $dispatcher)
+  {
+    $this->dispatcher = $dispatcher;
+  }
+
+  /**
+   * Registers a connection with the current manager.
+   * 
+   * @param Doctrine_Connection $conn     A Doctrine connection object
+   * @param string              $group    A connection group name
+   * @param boolean             $isMaster Whether this connection is a master
+   */
+  public function register(Doctrine_Connection $conn, $group = null, $isMaster 
= null)
+  {
+    if (null === $group)
+    {
+      $group = $this->getDefaultGroup();
+    }
+
+    if (!isset($this->connections[$group]))
+    {
+      // initialize group
+      $this->connections[$group] = array(
+        'master' => null,
+        'slaves' => array(),
+      );
+    }
+
+    if (null === $isMaster)
+    {
+      // assume this is the master if another hasn't been setup yet or if the 
name includes "master"
+      $isMaster = !isset($this->connections[$group]['master']) || false !== 
strpos($conn->getName(), 'master');
+    }
+
+    if ($isMaster)
+    {
+      // don't unregister any assumed master connections
+      if (isset($this->connections[$group]['master']))
+      {
+        $this->connections[$group]['slaves'][] = 
$this->connections[$group]['master'];
+      }
+
+      $this->connections[$group]['master'] = $conn->getName();
+    }
+    else
+    {
+      $this->connections[$group]['slaves'][] = $conn->getName();
+    }
+
+    $this->resetCurrentConnection();
+  }
+
+  /**
+   * Returns the master connection for a certain group.
+   * 
+   * @param string|Doctrine_Connection $group A Doctrine connection or 
connection group name
+   * 
+   * @return Doctrine_Connection A Doctrine connection object
+   * 
+   * @throws InvalidArgumentException If the group doesn't exists or doesn't 
have a master connection
+   */
+  public function getMasterConnection($group = null)
+  {
+    if (null === $group)
+    {
+      $group = $this->getDefaultGroup();
+    }
+
+    if ($group instanceof Doctrine_Connection)
+    {
+      $group = $this->getConnectionGroup($group);
+    }
+
+    if (!isset($this->connections[$group]['master']))
+    {
+      throw new InvalidArgumentException(sprintf('There is no master 
connection for the "%s" group', $group));
+    }
+
+    return 
$this->getDoctrineManager()->getConnection($this->connections[$group]['master']);
+  }
+
+  /**
+   * Returns a slave connection for a certain group.
+   * 
+   * @param string $group A connection group name
+   * 
+   * @return Doctrine_Connection A Doctrine connection object
+   * 
+   * @throws InvalidArgumentException If the group doesn't exists or doesn't 
have a slave connection
+   */
+  public function getSlaveConnection($group = null)
+  {
+    if (null === $group)
+    {
+      $group = $this->getDefaultGroup();
+    }
+
+    // use the master connection if we're in a transaction
+    $master = $this->getMasterConnection($group);
+    if ($master->getTransactionLevel())
+    {
+      return $master;
+    }
+
+    if (!isset($this->connections[$group]['current_slave']))
+    {
+      // select one slave
+      $event = $this->dispatcher->notifyUntil(new sfEvent($this, 
'doctrine.select_slave', array(
+        'group'  => $group,
+        'master' => $this->connections[$group]['master'],
+        'slaves' => $this->connections[$group]['slaves'],
+      )));
+
+      if ($event->isProcessed())
+      {
+        $this->connections[$group]['current_slave'] = $event->getReturnValue();
+      }
+      else if ($slaves = $this->connections[$group]['slaves'])
+      {
+        $this->connections[$group]['current_slave'] = 
$slaves[array_rand($slaves)];
+      }
+      else
+      {
+        $this->connections[$group]['current_slave'] = $master->getName();
+      }
+    }
+
+    return 
$this->getDoctrineManager()->getConnection($this->connections[$group]['current_slave']);
+  }
+
+  // protected
+
+  /**
+   * Returns the name of the current default group.
+   * 
+   * @return string A connection group name
+   */
+  protected function getDefaultGroup()
+  {
+    if (isset($this->connections[self::DEFAULT_GROUP]) || !$this->connections)
+    {
+      return self::DEFAULT_GROUP;
+    }
+    else
+    {
+      return key($this->connections);
+    }
+  }
+
+  /**
+   * Resets Doctrine's current connection to the default group's master 
connection.
+   */
+  protected function resetCurrentConnection()
+  {
+    if (isset($this->connections[$this->getDefaultGroup()]['master']))
+    {
+      
$this->getDoctrineManager()->setCurrentConnection($this->connections[$this->getDefaultGroup()]['master']);
+    }
+  }
+
+  /**
+   * Returns a group name based on connection name.
+   * 
+   * @param Doctrine_Connection $conn A Doctrine connection
+   * 
+   * @return string A connection group name
+   * 
+   * @throws InvalidArgumentException If the group name could not be determined
+   */
+  protected function getConnectionGroup(Doctrine_Connection $conn)
+  {
+    $name = $conn->getName();
+
+    foreach ($this->connections as $group => $connections)
+    {
+      if (
+        (isset($connections['master']) && $name == $connections['master'])
+        ||
+        (isset($connections['slaves']) && in_array($name, 
$connections['slaves']))
+      )
+      {
+        return $group;
+      }
+    }
+
+    throw new InvalidArgumentException(sprintf('Could not determine a group 
for the "%s" connection'));
+  }
+
+  /**
+   * A convenience method for returning the current Doctrine manager.
+   * 
+   * @return Doctrine_Manager The current Doctrine manager
+   */
+  protected function getDoctrineManager()
+  {
+    return Doctrine_Manager::getInstance();
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveConnectionManager.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveDebugListener.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveDebugListener.class.php
                               (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveDebugListener.class.php
       2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,83 @@
+<?php
+
+/**
+ * Emulates a read-only database connection by throwing exceptions.
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage connection
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+class sfDoctrineMasterSlaveDebugListener extends Doctrine_EventListener
+{
+  protected
+    $masterConnection = null;
+
+  /**
+   * Constructor.
+   * 
+   * @param Doctrine_Connection|sfCallable $masterConnection The master 
connection or an sfCallable that returns the master connection
+   * 
+   * @throws InvalidArgumentException If the argument is neither a connection 
nor sfCallable
+   */
+  public function __construct($masterConnection)
+  {
+    if (!$masterConnection instanceof Doctrine_Connection && 
!$masterConnection instanceof sfCallable)
+    {
+      throw new InvalidArgumentException('Argument must be either a connection 
object or sfCallable that returns a connection object.');
+    }
+
+    $this->masterConnection = $masterConnection;
+  }
+
+  /**
+   * Checks that the supplied connection is the master connection.
+   * 
+   * @param Doctrine_Connection $conn A connection to check
+   * 
+   * @throws LogicException If the connection is not the master connection
+   */
+  public function checkConnection(Doctrine_Connection $conn, $query = null)
+  {
+    if ($this->masterConnection instanceof sfCallable)
+    {
+      $this->masterConnection = $this->masterConnection->call();
+    }
+
+    if ($this->masterConnection !== $conn)
+    {
+      throw new LogicException('Cannot run this query on a read-only 
connection: '.$query);
+    }
+  }
+
+  public function preExec(Doctrine_Event $event)
+  {
+    if (0 !== stripos(trim($event->getQuery()), 'set'))
+    {
+      $this->checkConnection($event->getInvoker(), $event->getQuery());
+    }
+  }
+
+  public function prePrepare(Doctrine_Event $event)
+  {
+    if (!preg_match('/^(select|set)/i', trim($event->getQuery())))
+    {
+      $this->checkConnection($event->getInvoker(), $event->getQuery());
+    }
+  }
+
+  public function preTransactionBegin(Doctrine_Event $event)
+  {
+    $this->checkConnection($event->getInvoker()->getConnection(), 'BEGIN 
TRANSACTION');
+  }
+
+  public function preTransactionCommit(Doctrine_Event $event)
+  {
+    $this->checkConnection($event->getInvoker()->getConnection(), 'COMMIT');
+  }
+
+  public function preTransactionRollback(Doctrine_Event $event)
+  {
+    $this->checkConnection($event->getInvoker()->getConnection(), 'ROLLBACK');
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/connection/sfDoctrineMasterSlaveDebugListener.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/sfDoctrineMasterSlaveQuery.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/sfDoctrineMasterSlaveQuery.class.php
                            (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/sfDoctrineMasterSlaveQuery.class.php
    2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * Smartly uses either the master of slaves database connection.
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage query
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+class sfDoctrineMasterSlaveQuery extends Doctrine_Query
+{
+  /**
+   * Pre-query hook.
+   * 
+   * Sets the current query's connection based on what type of query is being 
run.
+   * 
+   * @see Doctrine_Query_Abstract
+   */
+  public function preQuery()
+  {
+    $method = Doctrine_Query::SELECT == $this->getType() ? 
'getSlaveConnection' : 'getMasterConnection';
+    
$this->setConnection(ProjectConfiguration::getActive()->$method($this->getConnection()));
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/query/sfDoctrineMasterSlaveQuery.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/sfDoctrineMasterSlaveRecord.class.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/sfDoctrineMasterSlaveRecord.class.php
                          (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/sfDoctrineMasterSlaveRecord.class.php
  2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * Overrides certain methods that require a master connection.
+ * 
+ * @package    sfDoctrineMasterSlavePlugin
+ * @subpackage record
+ * @author     Kris Wallsmith <[email protected]>
+ * @version    SVN: $Id$
+ */
+abstract class sfDoctrineMasterSlaveRecord extends sfDoctrineRecord
+{
+  /**
+   * Saves the current record.
+   * 
+   * Forces a master connection.
+   * 
+   * @see Doctrine_Record
+   */
+  public function save(Doctrine_Connection $conn = null)
+  {
+    $conn = ProjectConfiguration::getActive()->getMasterConnection($conn ? 
$conn : $this->getTable()->getConnection());
+
+    return parent::save($conn);
+  }
+
+  /**
+   * Replaces the current record.
+   * 
+   * Forces a master connection.
+   * 
+   * @see Doctrine_Record
+   */
+  public function replace(Doctrine_Connection $conn = null)
+  {
+    $conn = ProjectConfiguration::getActive()->getMasterConnection($conn ? 
$conn : $this->getTable()->getConnection());
+
+    return parent::replace($conn);
+  }
+
+  /**
+   * Deletes the current record from the database.
+   * 
+   * Forces a master connection.
+   * 
+   * @see Doctrine_Record
+   */
+  public function delete(Doctrine_Connection $conn = null)
+  {
+    $conn = ProjectConfiguration::getActive()->getMasterConnection($conn ? 
$conn : $this->getTable()->getConnection());
+
+    return parent::delete($conn);
+  }
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/lib/record/sfDoctrineMasterSlaveRecord.class.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: plugins/sfDoctrineMasterSlavePlugin/trunk/package.xml.tmpl
===================================================================
--- plugins/sfDoctrineMasterSlavePlugin/trunk/package.xml.tmpl                  
        (rev 0)
+++ plugins/sfDoctrineMasterSlavePlugin/trunk/package.xml.tmpl  2010-02-20 
01:11:48 UTC (rev 28144)
@@ -0,0 +1,63 @@
+<?xml version="1.0" encoding="##ENCODING##"?>
+<package xmlns="http://pear.php.net/dtd/package-2.0"; 
xmlns:tasks="http://pear.php.net/dtd/tasks-1.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; packagerversion="1.4.1" 
version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0 
http://pear.php.net/dtd/tasks-1.0.xsd http://pear.php.net/dtd/package-2.0 
http://pear.php.net/dtd/package-2.0.xsd";>
+  <name>sfDoctrineMasterSlavePlugin</name>
+  <channel>plugins.symfony-project.org</channel>
+  <summary>Easily manage master and slave database connections.</summary>
+  <description>Easily manage master and slave database 
connections.</description>
+  <lead>
+    <name>Kris Wallsmith</name>
+    <user>Kris.Wallsmith</user>
+    <email>[email protected]</email>
+    <active>yes</active>
+  </lead>
+  <date>##CURRENT_DATE##</date>
+  <version>
+    <release>##PLUGIN_VERSION##</release>
+    <api>##API_VERSION##</api>
+  </version>
+  <stability>
+    <release>stable</release>
+    <api>stable</api>
+  </stability>
+  <license uri="http://www.symfony-project.org/license";>MIT license</license>
+  <notes>-</notes>
+  <contents>
+    ##CONTENTS##
+  </contents>
+  <dependencies>
+    <required>
+      <php>
+        <min>5.2.4</min>
+      </php>
+      <pearinstaller>
+        <min>1.4.1</min>
+      </pearinstaller>
+      <package>
+        <name>symfony</name>
+        <channel>pear.symfony-project.com</channel>
+        <min>1.2.0</min>
+        <max>1.3.0</max>
+        <exclude>1.3.0</exclude>
+      </package>
+    </required>
+  </dependencies>
+  <phprelease></phprelease>
+  <changelog>
+    <release>
+      <version>
+        <release>1.0.0</release>
+        <api>1.0.0</api>
+      </version>
+      <stability>
+        <release>stable</release>
+        <api>stable</api>
+      </stability>
+      <license uri="http://www.symfony-project.com/license";>MIT 
license</license>
+      <license>MIT</license>
+      <date>2010-02-17</date>
+      <notes>
+        * Initial plugin release at symfony live 2010
+      </notes>
+    </release>
+  </changelog>
+</package>

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveConnectionManagerTest.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveConnectionManagerTest.php
                               (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveConnectionManagerTest.php
       2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * sfDoctrineMasterSlaveConnectionManager tests.
+ */
+include dirname(__FILE__).'/../../../../../test/bootstrap/unit.php';
+
+$t = new lime_test(19);
+
+$manager = Doctrine_Manager::getInstance();
+$pdo = new 
PDO('sqlite://'.sfConfig::get('sf_cache_dir').'/master_slave_test.sqlite');
+
+$conn1 = $manager->openConnection($pdo, 'doctrine');
+$conn2 = $manager->openConnection($pdo, 'slave');
+$conn3 = $manager->openConnection($pdo, 'master');
+
+class sfDoctrineMasterSlaveConnectionManagerTest extends 
sfDoctrineMasterSlaveConnectionManager
+{
+  public function getConnections()
+  {
+    return $this->connections;
+  }
+
+  public function setConnections(array $connections)
+  {
+    $this->connections = $connections;
+  }
+}
+
+$connectionManager = new 
sfDoctrineMasterSlaveConnectionManagerTest($configuration->getEventDispatcher());
+
+// ->register()
+$t->diag('->register()');
+
+$connectionManager->register($conn1);
+$connections = $connectionManager->getConnections();
+$t->is($connections['default']['master'], 'doctrine', '->register() marks the 
first connection as master');
+$t->is(Doctrine_Manager::connection()->getName(), 'doctrine', '->register() 
sets the master as the current connection');
+
+$connectionManager->register($conn2);
+$connections = $connectionManager->getConnections();
+$t->is_deeply($connections['default']['slaves'], array('slave'), '->register() 
saves additional connections as slaves');
+$t->is(Doctrine_Manager::connection()->getName(), 'doctrine', '->register() 
sets the master as the current connection');
+
+$connectionManager->register($conn3, null, true);
+$connections = $connectionManager->getConnections();
+$t->is($connections['default']['master'], 'master', '->register() sets a 
connection as master');
+$t->is_deeply($connections['default']['slaves'], array('slave', 'doctrine'), 
'->register() fixes master assumptions');
+$t->is(Doctrine_Manager::connection()->getName(), 'master', '->register() sets 
the master as the current connection');
+
+// ->getMasterConnection()
+$t->diag('->getMasterConnection()');
+
+$connectionManager->setConnections(array(
+  'default' => array(
+    'master' => 'master',
+    'slaves' => array('slave', 'doctrine'),
+)));
+$t->is($connectionManager->getMasterConnection()->getName(), 'master', 
'->getMasterConnection() returns the master connection');
+$t->is($connectionManager->getMasterConnection('default')->getName(), 
'master', '->getMasterConnection() accepts a group name');
+$t->is($connectionManager->getMasterConnection($conn3)->getName(), 'master', 
'->getMasterConnection() accepts a master connection');
+$t->is($connectionManager->getMasterConnection($conn2)->getName(), 'master', 
'->getMasterConnection() accepts a slave connection');
+
+$connectionManager->setConnections(array());
+try
+{
+  $connectionManager->getMasterConnection('default');
+  $t->fail('->getMasterConnection() throws an exception if there is not master 
connection');
+}
+catch (Exception $e)
+{
+  $t->pass('->getMasterConnection() throws an exception if there is not master 
connection');
+}
+
+$connectionManager->setConnections(array(
+  'foo' => array('master' => 'master'),
+  'bar' => array('master' => 'doctrine'),
+));
+$t->is($connectionManager->getMasterConnection()->getName(), 'master', 
'->getMasterConnection() defaults to using the first group registered');
+
+// ->getSlaveConnection()
+$t->diag('->getSlaveConnection()');
+
+$connectionManager->setConnections(array(
+  'default' => array(
+    'master' => 'master',
+    'slaves' => array('slave', 'doctrine'),
+)));
+$slave = $connectionManager->getSlaveConnection('default');
+$connections = $connectionManager->getConnections();
+$t->is($slave->getName(), $connections['default']['current_slave'], 
'->getSlaveConnection() returns a slave');
+$t->ok(in_array($connections['default']['current_slave'], 
$connections['default']['slaves']), '->getSlaveConnection() returns a slave');
+
+$slave = $connectionManager->getSlaveConnection();
+$connections = $connectionManager->getConnections();
+$t->is($slave->getName(), $connections['default']['current_slave'], 
'->getSlaveConnection() defaults to the default group');
+
+$conn3->beginTransaction();
+$t->is($connectionManager->getSlaveConnection('default')->getName(), 'master', 
'->getSlaveConnection() returns the master if a transaction is open');
+$conn3->rollback();
+$t->is($connectionManager->getSlaveConnection('default')->getName(), 
$slave->getName(), '->getSlaveConnection() returns the slave once a transaction 
is closed');
+
+$connectionManager->setConnections(array('default' => array(
+  'master' => 'master',
+  'slaves' => array(),
+)));
+$t->is($connectionManager->getSlaveConnection('default')->getName(), 'master', 
'->getSlaveConnection() returns the master if no slaves are registered');


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveConnectionManagerTest.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

Added: 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveDebugListenerTest.php
===================================================================
--- 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveDebugListenerTest.php
                           (rev 0)
+++ 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveDebugListenerTest.php
   2010-02-20 01:11:48 UTC (rev 28144)
@@ -0,0 +1,146 @@
+<?php
+
+/**
+ * sfDoctrineMasterSlaveDebugListener tests.
+ */
+include dirname(__FILE__).'/../../../../../test/bootstrap/unit.php';
+
+$t = new lime_test(11);
+
+$manager = Doctrine_Manager::getInstance();
+$pdo = new 
PDO('sqlite://'.sfConfig::get('sf_cache_dir').'/master_slave_test.sqlite');
+$master = $manager->openConnection($pdo, 'master');
+$slave = $manager->openConnection($pdo, 'slave');
+
+// ->__construct()
+$t->diag('->__construct()');
+
+try
+{
+  $listener = new sfDoctrineMasterSlaveDebugListener(new sfCallable(null));
+  $t->pass('->__construct() accepts a sfCallable');
+}
+catch (Exception $e)
+{
+  $t->fail('->__construct() accepts a sfCallable');
+  $t->diag('    '.$e->getMessage());
+}
+
+// ->checkConnection()
+$t->diag('->checkConnection()');
+
+$listener = new sfDoctrineMasterSlaveDebugListener($master);
+try
+{
+  $listener->checkConnection($master);
+  $t->pass('->checkConnection() does not throw an exception when passed the 
master connection');
+}
+catch (Exception $e)
+{
+  $t->fail('->checkConnection() does not throw an exception when passed the 
master connection');
+  $t->diag('    '.$e->getMessage());
+}
+try
+{
+  $listener->checkConnection($slave);
+  $t->fail('->checkConnection() throws an exception when passed a slave 
connection');
+}
+catch (Exception $e)
+{
+  $t->pass('->checkConnection() throws an exception when passed a slave 
connection');
+}
+
+// ->preExec()
+$t->diag('->preExec()');
+
+try
+{
+  $listener->preExec(new Doctrine_Event($slave, Doctrine_Event::CONN_EXEC, 
'SET NAMES ?', array('UTF-8')));
+  $t->pass('->preExec() allows SET queries to a slave connection');
+}
+catch (Exception $e)
+{
+  $t->fail('->preExec() allows SET queries to slave connections');
+  $t->diag('    '.$e->getMessage());
+}
+try
+{
+  $listener->preExec(new Doctrine_Event($slave, Doctrine_Event::CONN_EXEC, 
'UPDATE foo SET bar=?', array('test')));
+  $t->fail('->preExec() disallows UPDATE queries to slave connections');
+}
+catch (Exception $e)
+{
+  $t->pass('->preExec() disallows UPDATE queries to slave connections');
+}
+
+// ->prePrepare()
+$t->diag('->prePrepare()');
+
+try
+{
+  $listener->prePrepare(new Doctrine_Event($slave, 
Doctrine_Event::CONN_PREPARE, 'SELECT * FROM foo'));
+  $t->pass('->prePrepare() allows SELECT queries to slave connections');
+}
+catch (Exception $e)
+{
+  $t->fail('->prePrepare() allows SELECT queries to slave connections');
+  $t->diag('    '.$e->getMessage());
+}
+try
+{
+  $listener->prePrepare(new Doctrine_Event($slave, 
Doctrine_Event::CONN_PREPARE, 'SET NAMES ?', array('UTF-8')));
+  $t->pass('->prePrepare() allows SET queries to slave connections');
+}
+catch (Exception $e)
+{
+  $t->fail('->prePrepare() allows SET queries to slave connections');
+  $t->diag('    '.$e->getMessage());
+}
+try
+{
+  $listener->preExec(new Doctrine_Event($slave, Doctrine_Event::CONN_PREPARE, 
'UPDATE foo SET bar=?', array('test')));
+  $t->fail('->prePrepare() disallows UPDATE queries to slave connections');
+}
+catch (Exception $e)
+{
+  $t->pass('->prePrepare() disallows UPDATE queries to slave connections');
+}
+
+// ->preTransactionBegin()
+$t->diag('->preTransactionBegin()');
+
+try
+{
+  $listener->preTransactionBegin(new Doctrine_Event(new 
Doctrine_Transaction($slave), Doctrine_Event::TX_BEGIN));
+  $t->fail('->preTransactionBegin() disallows BEGIN TRANSACTION queries to 
slave connections');
+}
+catch (Exception $e)
+{
+  $t->pass('->preTransactionBegin() disallows BEGIN TRANSACTION queries to 
slave connections');
+}
+
+// ->preTransactionCommit()
+$t->diag('->preTransactionCommit()');
+
+try
+{
+  $listener->preTransactionCommit(new Doctrine_Event(new 
Doctrine_Transaction($slave), Doctrine_Event::TX_COMMIT));
+  $t->fail('->preTransactionCommit() disallows COMMIT queries to slave 
connections');
+}
+catch (Exception $e)
+{
+  $t->pass('->preTransactionCommit() disallows COMMIT queries to slave 
connections');
+}
+
+// ->preTransactionRollback()
+$t->diag('->preTransactionRollback()');
+
+try
+{
+  $listener->preTransactionRollback(new Doctrine_Event(new 
Doctrine_Transaction($slave), Doctrine_Event::TX_ROLLBACK));
+  $t->fail('->preTransactionRollback() disallows ROLLBACK queries to slave 
connections');
+}
+catch (Exception $e)
+{
+  $t->pass('->preTransactionRollback() disallows ROLLBACK queries to slave 
connections');
+}


Property changes on: 
plugins/sfDoctrineMasterSlavePlugin/trunk/test/unit/connection/sfDoctrineMasterSlaveDebugListenerTest.php
___________________________________________________________________
Added: svn:keywords
   + Id
Added: svn:eol-style
   + native

-- 
You received this message because you are subscribed to the Google Groups 
"symfony SVN" group.
To post to this group, send email to [email protected].
To unsubscribe from this group, send email to 
[email protected].
For more options, visit this group at 
http://groups.google.com/group/symfony-svn?hl=en.

Reply via email to