Db.php 17.5 KB
Newer Older
doctrine's avatar
doctrine committed
1
<?php
zYne's avatar
zYne committed
2
/*
3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
 *  $Id$
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * and is licensed under the LGPL. For more information, see
 * <http://www.phpdoctrine.com>.
 */
21 22 23 24
/**
 * Doctrine_Db
 * A thin wrapper layer on top of PDO / Doctrine_Adapter
 *
zYne's avatar
zYne committed
25
 * Doctrine_Db provides the following things to underlying database hanlder
26
 *
zYne's avatar
zYne committed
27 28 29 30
 * 1. Event listeners
 *    An easy to use, pluggable eventlistener architecture. Aspects such as
 *    logging, query profiling and caching can be easily implemented through
 *    the use of these listeners
31
 *
zYne's avatar
zYne committed
32 33 34 35 36 37 38 39 40 41 42 43 44
 * 2. Lazy-connecting
 *    Creating an instance of Doctrine_Db does not connect
 *    to database. Connecting to database is only invoked when actually needed
 *    (for example when query() is being called)
 *
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @package     Doctrine
 * @category    Object Relational Mapping
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 */
lsmith's avatar
lsmith committed
45 46
class Doctrine_Db implements Countable, IteratorAggregate, Doctrine_Adapter_Interface
{
47
    /**
48
     * @var array $instances            all the instances of this class
49 50
     */
    protected static $instances   = array();
doctrine's avatar
doctrine committed
51
    /**
52
     * @var array $isConnected          whether or not a connection has been established
doctrine's avatar
doctrine committed
53
     */
54
    protected $isConnected        = false;
doctrine's avatar
doctrine committed
55
    /**
56
     * @var PDO $dbh                    the database handler
doctrine's avatar
doctrine committed
57
     */
58
    protected $dbh;
doctrine's avatar
doctrine committed
59
    /**
60
     * @var array $options
doctrine's avatar
doctrine committed
61
     */
62 63 64 65
    protected $options            = array('dsn'      => null,
                                          'username' => null,
                                          'password' => null,
                                          );
66 67 68 69 70 71
    /**
     * @var array $pendingAttributes    An array of pending attributes. When setting attributes
     *                                  no connection is needed. When connected all the pending
     *                                  attributes are passed to the underlying PDO instance.
     */
    protected $pendingAttributes  = array();
72
    /**
lsmith's avatar
lsmith committed
73
     * @var Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
74 75 76
     *                              listener for listening events
     */
    protected $listener;
77 78 79 80 81
    /**
     * @var string $name                name of this connection
     * @see Doctrine_Manager::openConnection()
     */
    protected $name;
zYne's avatar
zYne committed
82 83
    
    protected $count = 0;
84

85 86 87 88 89 90 91

    private static $driverMap = array('oracle'     => 'oci8',
                                      'postgres'   => 'pgsql',
                                      'oci'        => 'oci8',
                                      'sqlite2'    => 'sqlite',
                                      'sqlite3'    => 'sqlite');

doctrine's avatar
doctrine committed
92 93
    /**
     * constructor
94 95 96 97
     *
     * @param string $dsn           data source name
     * @param string $user          database username
     * @param string $pass          database password
doctrine's avatar
doctrine committed
98
     */
zYne's avatar
zYne committed
99
    public function __construct($dsn, $user = null, $pass = null)
lsmith's avatar
lsmith committed
100
    {
zYne's avatar
zYne committed
101 102
    	// check if dsn is PEAR-like or not
        if ( ! isset($user) || strpos($dsn, '://')) {
103 104 105
            $a = self::parseDSN($dsn);

            extract($a);
106 107 108 109 110 111 112
        } else {
            $e = explode(':', $dsn);

            if($e[0] == 'uri') {
                $e[0] = 'odbc';
            }

113
            $this->pendingAttributes[Doctrine::ATTR_DRIVER_NAME] = $e[0];
114

115 116 117 118 119
        }
        $this->options['dsn']      = $dsn;
        $this->options['username'] = $user;
        $this->options['password'] = $pass;
        $this->listener = new Doctrine_Db_EventListener();
doctrine's avatar
doctrine committed
120
    }
zYne's avatar
zYne committed
121 122
    
    public function incrementQueryCount() 
lsmith's avatar
lsmith committed
123
    {
zYne's avatar
zYne committed
124
        $this->count++;
125 126
    }
    /**
zYne's avatar
zYne committed
127
     * getDbh
128
     */
zYne's avatar
zYne committed
129
    public function getDbh()
lsmith's avatar
lsmith committed
130
    {
zYne's avatar
zYne committed
131
        return $this->dbh;
132 133
    }
    /**
zYne's avatar
zYne committed
134 135 136
     * getAdapter
     *
     * @return Doctrine_Adapter_Interface|PDO $adapter
137
     */
zYne's avatar
zYne committed
138
    public function getAdapter()
lsmith's avatar
lsmith committed
139
    {
140 141
        return $this->dbh;
    }
142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172
    /**
     * setAdapter
     *
     * @param Doctrine_Adapter_Interface|PDO $adapter
     * @return void
     */
    public function setAdapter($adapter)
    {
        $this->dbh = $adapter;
    }
    /**
     * setName
     * this method should only be used by doctrine internally and
     * also for testing purposes
     *
     * @param string $name      connection name
     * @return void
     */
    public function setName($name)
    {
        $this->name = $name;
    }
    /**
     * getName
     *
     * @return string           the name of the associated connection
     */
    public function getName()
    {
        return $this->name;
    }
lsmith's avatar
lsmith committed
173 174
    public function getOption($name)
    {
lsmith's avatar
lsmith committed
175
        if ( ! array_key_exists($name, $this->options)) {
176
            throw new Doctrine_Db_Exception('Unknown option ' . $name);
lsmith's avatar
lsmith committed
177
        }
178
        return $this->options[$name];
doctrine's avatar
doctrine committed
179
    }
180 181 182 183 184 185
    /**
     * addListener
     *
     * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
     * @return Doctrine_Db
     */
lsmith's avatar
lsmith committed
186 187
    public function addListener($listener, $name = null)
    {
lsmith's avatar
lsmith committed
188
        if ( ! ($this->listener instanceof Doctrine_Db_EventListener_Chain)) {
189
            $this->listener = new Doctrine_Db_EventListener_Chain();
lsmith's avatar
lsmith committed
190
        }
191
        $this->listener->add($listener, $name);
lsmith's avatar
lsmith committed
192

193 194 195 196
        return $this;
    }
    /**
     * getListener
lsmith's avatar
lsmith committed
197
     *
198 199
     * @return Doctrine_Db_EventListener_Interface|Doctrine_Overloadable
     */
lsmith's avatar
lsmith committed
200 201
    public function getListener()
    {
202 203
        return $this->listener;
    }
doctrine's avatar
doctrine committed
204
    /**
205 206 207 208
     * setListener
     *
     * @param Doctrine_Db_EventListener_Interface|Doctrine_Overloadable $listener
     * @return Doctrine_Db
doctrine's avatar
doctrine committed
209
     */
lsmith's avatar
lsmith committed
210 211
    public function setListener($listener)
    {
lsmith's avatar
lsmith committed
212 213 214
        if ( ! ($listener instanceof Doctrine_Db_EventListener_Interface)
            && ! ($listener instanceof Doctrine_Overloadable)
        ) {
215
            throw new Doctrine_Db_Exception("Couldn't set eventlistener for database handler. EventListeners should implement either Doctrine_Db_EventListener_Interface or Doctrine_Overloadable");
lsmith's avatar
lsmith committed
216
        }
217
        $this->listener = $listener;
doctrine's avatar
doctrine committed
218

219 220 221 222 223 224 225 226 227
        return $this;
    }

    /**
     * connect
     * connects into database
     *
     * @return boolean
     */
lsmith's avatar
lsmith committed
228 229
    public function connect()
    {
lsmith's avatar
lsmith committed
230
        if ($this->isConnected)
231 232
            return false;

233 234 235 236
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::CONNECT);

        $this->listener->onPreConnect($event);

237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258
        $e     = explode(':', $this->options['dsn']);
        $found = false;
        
        if (extension_loaded('pdo')) {
            if (in_array($e[0], PDO::getAvailableDrivers())) {
                $this->dbh = new PDO($this->options['dsn'], $this->options['username'], $this->options['password']);
                $this->dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
                //$this->dbh->setAttribute(PDO::ATTR_STATEMENT_CLASS, array('Doctrine_Db_Statement', array($this)));
                $found = true;
            }
        }

        if ( ! $found) {
            $class = 'Doctrine_Adapter_' . ucwords($e[0]);

            if (class_exists($class)) {
                $this->dbh = new $class($this->options['dsn'], $this->options['username'], $this->options['password']);
            } else {
                throw new Doctrine_Db_Exception("Couldn't locate driver named " . $e[0]);      	
            }
        }

259 260
        
        foreach($this->pendingAttributes as $attr => $value) {
zYne's avatar
zYne committed
261
            // some drivers don't support setting this so we just skip it
262
            if($attr == Doctrine::ATTR_DRIVER_NAME) {
zYne's avatar
zYne committed
263 264
                continue;
            }
265 266 267
            $this->dbh->setAttribute($attr, $value);
        }

268
        $this->isConnected = true;
269 270

        $this->listener->onConnect($event);
271 272 273 274 275 276 277 278 279 280 281
        return true;
    }

    /**
     * getConnection
     *
     * @param string $dsn               PEAR::DB like DSN or PDO like DSN
     * format for PEAR::DB like DSN:    schema://user:password@address/dbname
     *
     * @return
     */
lsmith's avatar
lsmith committed
282 283
    public static function getConnection($dsn = null, $username = null, $password = null)
    {
284 285 286 287
        return new self($dsn, $username, $password);
    }
    /**
     * driverName
lsmith's avatar
lsmith committed
288
     * converts a driver name like (oracle) to appropriate PDO
289 290 291 292 293
     * driver name (oci8 in the case of oracle)
     *
     * @param string $name
     * @return string
     */
lsmith's avatar
lsmith committed
294 295
    public static function driverName($name)
    {
lsmith's avatar
lsmith committed
296
        if (isset(self::$driverMap[$name])) {
297
            return self::$driverMap[$name];
lsmith's avatar
lsmith committed
298
        }
299 300 301 302 303
        return $name;
    }
    /**
     * parseDSN
     *
lsmith's avatar
lsmith committed
304 305
     * @param string $dsn
     * @return array Parsed contents of DSN
306
     */
307
    public function parseDSN($dsn)
lsmith's avatar
lsmith committed
308
    {
309
        // silence any warnings
lsmith's avatar
lsmith committed
310 311
        $parts = @parse_url($dsn);

312
        $names = array('scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment');
lsmith's avatar
lsmith committed
313 314 315

        foreach ($names as $name) {
            if ( ! isset($parts[$name])) {
316
                $parts[$name] = null;
lsmith's avatar
lsmith committed
317
            }
doctrine's avatar
doctrine committed
318
        }
319

lsmith's avatar
lsmith committed
320 321 322
        if (count($parts) == 0 || ! isset($parts['scheme'])) {
            throw new Doctrine_Db_Exception('Empty data source name');
        }
323 324 325
        $drivers = self::getAvailableDrivers();

        $parts['scheme'] = self::driverName($parts['scheme']);
zYne's avatar
zYne committed
326
        /**
lsmith's avatar
lsmith committed
327
        if ( ! in_array($parts['scheme'], $drivers)) {
328
            throw new Doctrine_Db_Exception('Driver '.$parts['scheme'].' not availible or extension not loaded');
lsmith's avatar
lsmith committed
329
        }
zYne's avatar
zYne committed
330
        */
lsmith's avatar
lsmith committed
331
        switch ($parts['scheme']) {
332 333 334 335 336
            case 'sqlite':
                if (isset($parts['host']) && $parts['host'] == ':memory') {
                    $parts['database'] = ':memory:';
                    $parts['dsn']      = 'sqlite::memory:';
                }
lsmith's avatar
lsmith committed
337

338 339 340 341 342 343
                break;
            case 'mysql':
            case 'informix':
            case 'oci8':
            case 'mssql':
            case 'firebird':
zYne's avatar
zYne committed
344
            case 'dblib':
345 346
            case 'pgsql':
            case 'odbc':
zYne's avatar
zYne committed
347
            case 'mock':
348 349 350 351 352 353 354 355 356 357 358 359 360
                if ( ! isset($parts['path']) || $parts['path'] == '/') {
                    throw new Doctrine_Db_Exception('No database availible in data source name');
                }
                if (isset($parts['path'])) {
                    $parts['database'] = substr($parts['path'], 1);
                }
                if ( ! isset($parts['host'])) {
                    throw new Doctrine_Db_Exception('No hostname set in data source name');
                }
                $parts['dsn'] = $parts["scheme"].":host=".$parts["host"].";dbname=".$parts["database"];
                break;
            default:
                throw new Doctrine_Db_Exception('Unknown driver '.$parts['scheme']);
lsmith's avatar
lsmith committed
361
        }
362
        $this->pendingAttributes[PDO::ATTR_DRIVER_NAME] = $parts['scheme'];
363

lsmith's avatar
lsmith committed
364 365
        return $parts;
    }
366 367 368 369 370 371
    /**
     * clear
     * clears all instances from the memory
     *
     * @return void
     */
lsmith's avatar
lsmith committed
372 373
    public static function clear()
    {
374 375 376 377 378 379 380 381 382
        self::$instances = array();
    }

    /**
     * errorCode
     * Fetch the SQLSTATE associated with the last operation on the database handle
     *
     * @return integer
     */
lsmith's avatar
lsmith committed
383 384
    public function errorCode()
    {
385
        return $this->dbh->errorCode();
doctrine's avatar
doctrine committed
386 387
    }
    /**
388 389 390 391
     * errorInfo
     * Fetch extended error information associated with the last operation on the database handle
     *
     * @return array
doctrine's avatar
doctrine committed
392
     */
lsmith's avatar
lsmith committed
393 394
    public function errorInfo()
    {
395 396 397 398 399 400 401
        return $this->dbh->errorInfo();
    }
    /**
     * prepare
     *
     * @param string $statement
     */
lsmith's avatar
lsmith committed
402 403
    public function prepare($statement)
    {
404
        $this->connect();
doctrine's avatar
doctrine committed
405

406
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::PREPARE, $statement);
doctrine's avatar
doctrine committed
407

408 409 410 411 412 413
        $this->listener->onPrePrepare($event);

        $stmt = $this->dbh->prepare($statement);

        $this->listener->onPrepare($event);

414
        return new Doctrine_Db_Statement($this, $stmt);
415 416 417 418 419 420 421 422 423 424
    }
    /**
     * query
     *
     * @param string $statement
     * @param array $params
     * @return Doctrine_Db_Statement|boolean
     */
    public function query($statement, array $params = array()) {
        $this->connect();
lsmith's avatar
lsmith committed
425

426 427
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::QUERY, $statement);

zYne's avatar
zYne committed
428
        $skip  = $this->listener->onPreQuery($event);
429

lsmith's avatar
lsmith committed
430
        if ( ! empty($params)) {
zYne's avatar
zYne committed
431
            $stmt = $this->dbh->prepare($statement);
zYne's avatar
zYne committed
432

zYne's avatar
zYne committed
433
            return $stmt->execute($params);
zYne's avatar
zYne committed
434

lsmith's avatar
lsmith committed
435
        } else {
zYne's avatar
zYne committed
436 437 438 439 440 441 442 443
            if ( ! $skip) {
                $stmt = $this->dbh->query($statement);
                $this->count++;
            } else {
                $stmt = new stdClass;
                $stmt->queryString = $statement;
            }
            $stmt = new Doctrine_Db_Statement($this, $stmt);
lsmith's avatar
lsmith committed
444
        }
445

zYne's avatar
zYne committed
446
        $this->listener->onQuery($event);
447 448

        return $stmt;
doctrine's avatar
doctrine committed
449 450
    }
    /**
451 452 453 454 455
     * quote
     * quotes a string for use in a query
     *
     * @param string $input
     * @return string
doctrine's avatar
doctrine committed
456
     */
lsmith's avatar
lsmith committed
457 458
    public function quote($input)
    {
459
        $this->connect();
460

461
        return $this->dbh->quote($input);
doctrine's avatar
doctrine committed
462 463
    }
    /**
464 465 466 467 468 469
     * exec
     * executes an SQL statement and returns the number of affected rows
     *
     * @param string $statement
     * @return integer
     */
lsmith's avatar
lsmith committed
470 471
    public function exec($statement)
    {
472 473 474
        $this->connect();

        $args = func_get_args();
lsmith's avatar
lsmith committed
475

476 477 478 479 480 481
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::EXEC, $statement);

        $this->listener->onPreExec($event);

        $rows = $this->dbh->exec($statement);

zYne's avatar
zYne committed
482 483
        $this->count++;

484 485 486 487
        $this->listener->onExec($event);

        return $rows;
    }
488

489 490 491 492
    /**
     * lastInsertId
     *
     * @return integer
doctrine's avatar
doctrine committed
493
     */
lsmith's avatar
lsmith committed
494 495
    public function lastInsertId()
    {
496 497 498
        $this->connect();

        return $this->dbh->lastInsertId();
doctrine's avatar
doctrine committed
499
    }
500 501 502 503 504
    /**
     * begins a transaction
     *
     * @return boolean
     */
lsmith's avatar
lsmith committed
505 506
    public function beginTransaction()
    {
507 508
        $this->connect();

509 510 511 512 513 514 515
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::BEGIN);

        $this->listener->onPreBeginTransaction($event);

        $return = $this->dbh->beginTransaction();

        $this->listener->onBeginTransaction($event);
lsmith's avatar
lsmith committed
516

517
        return $return;
doctrine's avatar
doctrine committed
518 519
    }
    /**
520 521 522
     * commits a transaction
     *
     * @return boolean
doctrine's avatar
doctrine committed
523
     */
lsmith's avatar
lsmith committed
524 525
    public function commit()
    {
526
        $this->connect();
zYne's avatar
zYne committed
527

528 529 530 531 532 533 534 535 536
        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::COMMIT);

        $this->listener->onPreCommit($event);

        $return = $this->dbh->commit();

        $this->listener->onCommit($event);

        return $return;
doctrine's avatar
doctrine committed
537 538
    }
    /**
539 540 541 542
     * rollBack
     *
     * @return boolean
     */
lsmith's avatar
lsmith committed
543 544
    public function rollBack()
    {
545 546 547 548 549 550 551
        $this->connect();

        $event = new Doctrine_Db_Event($this, Doctrine_Db_Event::ROLLBACK);

        $this->listener->onPreRollback($event);

        $this->dbh->rollBack();
lsmith's avatar
lsmith committed
552

553 554 555 556 557 558 559 560 561
        $this->listener->onRollback($event);
    }
    /**
     * getAttribute
     * retrieves a database connection attribute
     *
     * @param integer $attribute
     * @return mixed
     */
lsmith's avatar
lsmith committed
562 563
    public function getAttribute($attribute)
    {
564 565 566 567 568 569 570 571 572
        if ($this->isConnected) {
            return $this->dbh->getAttribute($attribute);
        } else {
            if ( ! isset($this->pendingAttributes[$attribute])) {
                throw new Doctrine_Db_Exception('Attribute ' . $attribute . ' not found.');
            }
            
            return $this->pendingAttributes[$attribute];
        }
573 574 575 576
    }
    /**
     * returns an array of available PDO drivers
     */
lsmith's avatar
lsmith committed
577 578
    public static function getAvailableDrivers()
    {
579 580 581 582 583 584 585 586 587 588
        return PDO::getAvailableDrivers();
    }
    /**
     * setAttribute
     * sets an attribute
     *
     * @param integer $attribute
     * @param mixed $value
     * @return boolean
     */
lsmith's avatar
lsmith committed
589 590
    public function setAttribute($attribute, $value)
    {
591 592 593 594 595
        if ($this->isConnected) {
            $this->dbh->setAttribute($attribute, $value);
        } else {
            $this->pendingAttributes[$attribute] = $value;
        }
596 597 598 599
    }
    /**
     * getIterator
     *
doctrine's avatar
doctrine committed
600 601
     * @return ArrayIterator
     */
lsmith's avatar
lsmith committed
602 603
    public function getIterator()
    {
zYne's avatar
zYne committed
604
        if ($this->listener instanceof Doctrine_Db_Profiler) {
605
            return $this->listener;
zYne's avatar
zYne committed
606
        }
doctrine's avatar
doctrine committed
607 608
    }
    /**
609
     * count
doctrine's avatar
doctrine committed
610
     * returns the number of executed queries
611
     *
doctrine's avatar
doctrine committed
612 613
     * @return integer
     */
lsmith's avatar
lsmith committed
614 615
    public function count()
    {
zYne's avatar
zYne committed
616
        return $this->count;
lsmith's avatar
lsmith committed
617
    }
doctrine's avatar
doctrine committed
618
}