Location: PHPKode > projects > SolarPHP > solar-system-1.1.1/solar/source/solar/Solar/Sql/Adapter/MysqlReplicated.php
<?php
/**
 * 
 * Uses a random slave server for SELECT queries, and a master server for all
 * other queries.
 * 
 * Multiple slaves can be configured, but once we start reading from a slave,
 * we read from that slave for the remainder of the connection.  (Invoking
 * disconnect() will let you connect to new random slave.)
 * 
 * @category Solar
 * 
 * @package Solar_Sql
 * 
 * @author Paul M. Jones <hide@address.com>
 * 
 * @license http://opensource.org/licenses/bsd-license.php BSD
 * 
 * @version $Id: MysqlReplicated.php 4533 2010-04-23 16:35:15Z pmjones $
 * 
 */
class Solar_Sql_Adapter_MysqlReplicated extends Solar_Sql_Adapter_Mysql
{
    /**
     * 
     * Default configuration values.
     * 
     * @config dependency request A Solar_Request dependecy.  Defaults to the 'request'
     *   registry entry.
     * 
     * @config array slaves An array of arrays, each representing the connection values
     *   for a different slave server.
     * 
     * The non-slave connection values are for the master server.
     * 
     * For example:
     * 
     * {{code: php
     *     $config = array(
     *     
     *         // these apply to the master and all slaves
     *         'profiling' => false,
     *         'cache'     => array('adapter' => 'Solar_Cache_Adapter_Var'),
     *         
     *         // master server connection
     *         'host'      => null,
     *         'port'      => null,
     *         'sock'      => null,
     *         'user'      => null,
     *         'pass'      => null,
     *         'name'      => null,
     *         
     *         // all slave servers
     *         'slaves'    => array(
     *             
     *             // first slave server
     *             0 => array(
     *                 'host'      => null,
     *                 'port'      => null,
     *                 'sock'      => null,
     *                 'user'      => null,
     *                 'pass'      => null,
     *                 'name'      => null,
     *             ),
     *             
     *             // second slave server
     *             1 => array(
     *                 'host'      => null,
     *                 'port'      => null,
     *                 'sock'      => null,
     *                 'user'      => null,
     *                 'pass'      => null,
     *                 'name'      => null,
     *             ),
     *             
     *             // ... etc ...
     *         ),
     *     );
     * }}
     * 
     * @var array
     * 
     */
    protected $_Solar_Sql_Adapter_MysqlReplicated = array(
        'request' => 'request',
        'slaves' => array(),
    );
    
    /**
     * 
     * Array of slave connection parameters for DSNs.
     * 
     * @var array
     * 
     */
    protected $_slaves = array();
    
    /**
     * 
     * Which slave key the `$_dsn` property was built from.
     * 
     * @var mixed
     * 
     */
    protected $_slave_key;
    
    /**
     * 
     * A PDO-style DSN for the master server.
     * 
     * The `$_dsn` property is for the slave server.
     * 
     * @var string
     * 
     */
    protected $_dsn_master;
    
    /**
     * 
     * A PDO object for accessing the master server.
     * 
     * The `$_pdo` property is for the slave server.
     * 
     * @var PDO
     * 
     * @see $_pdo
     * 
     */
    protected $_pdo_master;
    
    /**
     * 
     * Is the current request a GET-after-POST/PUT?
     * 
     * @var bool
     * 
     */
    protected $_is_gap;
    
    /**
     * 
     * Whether or not we're in a transaction.
     * 
     * When true, all SELECTs go to the master.
     * 
     * @var bool
     * 
     */
    protected $_in_transaction = false;
    
    /**
     * 
     * Follow-on setup for the constructor to build the $_slaves array.
     * 
     * @return void
     * 
     * @see $_slaves
     * 
     */
    protected function _setup()
    {
        // build up the $_slaves array info, using some of the values from
        // the master as defaults
        $base = array(
            'host' => null,
            'port' => $this->_config['port'],
            'sock' => null,
            'user' => $this->_config['user'],
            'pass' => $this->_config['pass'],
            'name' => $this->_config['name'],
        );
        
        foreach ($this->_config['slaves'] as $key => $val) {
            $this->_slaves[$key] = array_merge($base, $val);
        }
        
        // is this a GET-after-POST/PUT request?
        $request = Solar::dependency(
            'Solar_Request',
            $this->_config['request']
        );
        $this->_is_gap = $request->isGap();
        
        // done, on to the main setup
        parent::_setup();
    }
    
    /**
     * 
     * Sets the connection-specific cache key prefix.
     * 
     * @param string $prefix The cache-key prefix.  When null, defaults to
     * the class name, a slash, and the md5() of the DSN **for the master**.
     * 
     * @return string
     * 
     */
    public function setCacheKeyPrefix($prefix = null)
    {
        if ($prefix === null) {
            $prefix = get_class($this) . '/' . md5($this->_dsn_master);
        }
        
        $this->_cache_key_prefix = $prefix;
    }
    
    /**
     * 
     * Get the master PDO connection object (connects to the database if 
     * needed).
     * 
     * @return PDO
     * 
     */
    public function getPdoMaster()
    {
        $this->connectMaster();
        return $this->_pdo_master;
    }
    
    /**
     * 
     * Sets the DSN for the slave and the master; the slave is picked at 
     * random from the list of slaves.
     * 
     * For example, "mysql:host=127.0.0.1;dbname=test"
     * 
     * For the keys 'port', 'user', 'pass', and 'name', if one is missing, it
     * gets set automatically from the master value. This lets you avoid some
     * repetition in your slave config setups, assuming that the values are 
     * the same for the master and slaves.
     * 
     * @return void
     * 
     * @see $_dsn
     * 
     * @see $_slave_key
     * 
     * @see $_dsn_master
     * 
     */
    protected function _setDsn()
    {
        // pick a random slave key ... doing it this way lets us use string
        // keys for slave configs.
        $keys = array_keys($this->_slaves);
        $rand = array_rand($keys);
        $this->_slave_key = $keys[$rand];
        
        // get the slave info
        $slave = $this->_slaves[$this->_slave_key];
        
        // set DSN for slave
        $this->_dsn = $this->_buildDsn($slave);
        
        // set DSN for master
        $this->_dsn_master = $this->_buildDsn($this->_config);
    }
    
    /**
     * 
     * Connects to a random slave server.
     * 
     * Does not re-connect if we already have active connections.
     * 
     * @return void
     * 
     */
    public function connect()
    {
        // already connected?
        if ($this->_pdo) {
            return;
        }
        
        // which slave dsn key was used?
        // need this so we have the right credentials.
        $key = $this->_slave_key;
        
        // start profile time
        $time = microtime(true);
        
        // attempt the connection
        $this->_pdo = new PDO(
            $this->_dsn,
            $this->_slaves[$key]['user'],
            $this->_slaves[$key]['pass']
        );
        
        // retain connection info
        $this->_pdo->solar_conn = array(
            'dsn'  => $this->_dsn,
            'user' => $this->_slaves[$key]['user'],
            'pass' => $this->_slaves[$key]['pass'],
            'type' => 'slave',
            'key'  => $key,
        );
        
        // post-connection tasks
        $this->_postConnect();
        
        // retain the profile data?
        $this->_addProfile($time, '__CONNECT');
    }
    
    /**
     * 
     * Connects to the master server.
     * 
     * Does not re-connect if we already have an active connection.
     * 
     * @return void
     * 
     */
    public function connectMaster()
    {
        // already connected?
        if ($this->_pdo_master) {
            return;
        }
        
        // need a slave connection first
        $this->connect();
        
        // start profile time
        $time = microtime(true);
        
        // attempt the connection
        $this->_pdo_master = new PDO(
            $this->_dsn_master,
            $this->_config['user'],
            $this->_config['pass']
        );
        
        // retain connection info
        $this->_pdo_master->solar_conn = array(
            'dsn'  => $this->_dsn_master,
            'user' => $this->_config['user'],
            'pass' => $this->_config['pass'],
            'type' => 'master',
            'key'  => null,
        );
        
        // post-connection tasks
        $this->_postConnectMaster();
        
        // retain the profile data?
        $this->_addProfile($time, '__CONNECT_MASTER');
    }
    
    /**
     * 
     * Force the master connection to use the same attributes as the slave.
     * 
     * @return void
     * 
     */
    protected function _postConnectMaster()
    {
        // adapted from example at
        // <http://php.net/manual/en/pdo.getattribute.php>.
        // uses extra attribs listed at <http://php.net/pdo_mysql>.
        $attribs = array(
            'ATTR_AUTOCOMMIT',
            'ATTR_CASE',
            'ATTR_CLIENT_VERSION',
            'ATTR_CONNECTION_STATUS',
            'ATTR_ERRMODE',
            'ATTR_ORACLE_NULLS',
            'ATTR_PERSISTENT',
            // 'ATTR_PREFETCH', // not supported by driver
            'ATTR_SERVER_INFO',
            'ATTR_SERVER_VERSION',
            // 'ATTR_TIMEOUT', // not supported by driver
        );
        
        foreach ($attribs as $attr) {
            $key = constant("PDO::$attr");
            $val = $this->_pdo->getAttribute($key);
            $this->_pdo_master->setAttribute($key, $val);
        }
    }
    
    /**
     * 
     * Disconnects from the master and the slave.
     * 
     * @return void
     * 
     */
    public function disconnect()
    {
        parent::diconnect();
        $this->_pdo_master = null;
    }
    
    /**
     * 
     * Prepares an SQL query as a PDOStatement object, using the slave PDO
     * connection for all SELECT queries outside a transation, and the master
     * PDO connection for all other queries (incl. in-transaction SELECTs).
     * Note also that a GET-after-POST request will force all reads and writes
     * to use the master.
     * 
     * @param string $stmt The text of the SQL statement, optionally with
     * named placeholders.
     * 
     * @return PDOStatement
     * 
     */
    protected function _prepare($stmt)
    {
        // clean up the statement a bit
        $stmt = ltrim($stmt);
        
        // use the slave on SELECT statements, but only when we're not in
        // a transaction, and also not in a GET-after-POST request.
        $use_slave = strtoupper(substr($stmt, 0, 6)) == 'SELECT'
                  && ! $this->_in_transaction
                  && ! $this->_is_gap;
        
        // prepare the statment
        try {
            if ($use_slave) {
                // use a slave
                $config = $this->_slaves[$this->_slave_key];
                $this->connect();
                $prep = $this->_pdo->prepare($stmt);
                $prep->solar_conn = $this->_pdo->solar_conn;
            } else {
                // use the master
                $config = $this->_config;
                $this->connectMaster();
                $prep = $this->_pdo_master->prepare($stmt);
                $prep->solar_conn = $this->_pdo_master->solar_conn;
            }
        } catch (PDOException $e) {
            // note that we use $config as set in the try block above
            throw $this->_exception('ERR_PREPARE_FAILED', array(
                'pdo_code'  => $e->getCode(),
                'pdo_text'  => $e->getMessage(),
                'host'      => $config['host'],
                'port'      => $config['port'],
                'sock'      => $config['sock'],
                'user'      => $config['user'],
                'name'      => $config['name'],
                'stmt'      => $stmt,
                'pdo_trace' => $e->getTraceAsString(),
            ));
        }
        
        return $prep;
    }
    
    /**
     * 
     * Leave autocommit mode and begin a transaction **on the master**.
     * 
     * @return void
     * 
     */
    public function begin()
    {
        $this->connectMaster();
        $time = microtime(true);
        $result = $this->_pdo_master->beginTransaction();
        $this->_in_transaction = true;
        $this->_addProfile($time, '__BEGIN_MASTER');
        return $result;
    }
    
    /**
     * 
     * Commit a transaction and return to autocommit mode **on the master**.
     * 
     * @return void
     * 
     */
    public function commit()
    {
        $this->connectMaster();
        $time = microtime(true);
        $result = $this->_pdo_master->commit();
        $this->_in_transaction = false;
        $this->_addProfile($time, '__COMMIT_MASTER');
        return $result;
    }
    
    /**
     * 
     * Roll back a transaction and return to autocommit mode **on the master**.
     * 
     * @return void
     * 
     */
    public function rollback()
    {
        $this->connectMaster();
        $time = microtime(true);
        $result = $this->_pdo_master->rollBack();
        $this->_in_transaction = false;
        $this->_addProfile($time, '__ROLLBACK_MASTER');
        return $result;
    }
    
    /**
     * 
     * Get the last auto-incremented insert ID from the database.
     * 
     * @param string $table The table name on which the auto-increment occurred.
     * 
     * @param string $col The name of the auto-increment column.
     * 
     * @return int The last auto-increment ID value inserted to the database.
     * 
     */
    public function lastInsertId($table = null, $col = null)
    {
        $this->connectMaster();
        return $this->_pdo_master->lastInsertId();
    }
}
Return current item: SolarPHP