Manager.php 20.1 KB
Newer Older
lsmith's avatar
lsmith committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
<?php
/*
 *  $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>.
 */
/**
 *
lsmith's avatar
lsmith committed
23
 * Doctrine_Manager is the base component of all doctrine based projects.
lsmith's avatar
lsmith committed
24 25 26
 * It opens and keeps track of all connections (database connections).
 *
 * @package     Doctrine
27
 * @subpackage  Manager
lsmith's avatar
lsmith committed
28 29 30 31 32 33
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 */
lsmith's avatar
lsmith committed
34 35
class Doctrine_Manager extends Doctrine_Configurable implements Countable, IteratorAggregate
{
lsmith's avatar
lsmith committed
36
    /**
zYne's avatar
zYne committed
37
     * @var array $connections          an array containing all the opened connections
lsmith's avatar
lsmith committed
38
     */
39
    protected $_connections   = array();
lsmith's avatar
lsmith committed
40
    /**
zYne's avatar
zYne committed
41
     * @var array $bound                an array containing all components that have a bound connection
lsmith's avatar
lsmith committed
42
     */
43
    protected $_bound         = array();
lsmith's avatar
lsmith committed
44
    /**
zYne's avatar
zYne committed
45
     * @var integer $index              the incremented index
lsmith's avatar
lsmith committed
46
     */
47
    protected $_index         = 0;
lsmith's avatar
lsmith committed
48
    /**
zYne's avatar
zYne committed
49
     * @var integer $currIndex          the current connection index
lsmith's avatar
lsmith committed
50
     */
51
    protected $_currIndex     = 0;
lsmith's avatar
lsmith committed
52
    /**
zYne's avatar
zYne committed
53
     * @var string $root                root directory
lsmith's avatar
lsmith committed
54
     */
55
    protected $_root;
zYne's avatar
zYne committed
56
    /**
zYne's avatar
zYne committed
57
     * @var Doctrine_Query_Registry     the query registry
zYne's avatar
zYne committed
58
     */
zYne's avatar
zYne committed
59
    protected $_queryRegistry;
zYne's avatar
zYne committed
60
    
zYne's avatar
zYne committed
61
    protected static $driverMap = array('oci' => 'oracle');
lsmith's avatar
lsmith committed
62 63 64 65 66
    /**
     * constructor
     *
     * this is private constructor (use getInstance to get an instance of this class)
     */
lsmith's avatar
lsmith committed
67 68
    private function __construct()
    {
69
        $this->_root = dirname(__FILE__);
lsmith's avatar
lsmith committed
70

71
        Doctrine_Locator_Injectable::initNullObject(new Doctrine_Null);
lsmith's avatar
lsmith committed
72 73 74 75 76 77 78
    }
    /**
     * setDefaultAttributes
     * sets default attributes
     *
     * @return boolean
     */
79
    public function setDefaultAttributes()
lsmith's avatar
lsmith committed
80
    {
lsmith's avatar
lsmith committed
81 82 83 84
        static $init = false;
        if ( ! $init) {
            $init = true;
            $attributes = array(
zYne's avatar
zYne committed
85
                        Doctrine::ATTR_CACHE            => null,
86
                        Doctrine::ATTR_LOAD_REFERENCES  => true,
lsmith's avatar
lsmith committed
87
                        Doctrine::ATTR_LISTENER         => new Doctrine_EventListener(),
88
                        Doctrine::ATTR_RECORD_LISTENER  => new Doctrine_Record_Listener(),
zYne's avatar
zYne committed
89
                        Doctrine::ATTR_THROW_EXCEPTIONS => true,
90
                        Doctrine::ATTR_VALIDATE         => Doctrine::VALIDATE_NONE,
lsmith's avatar
lsmith committed
91 92 93 94 95 96
                        Doctrine::ATTR_QUERY_LIMIT      => Doctrine::LIMIT_RECORDS,
                        Doctrine::ATTR_IDXNAME_FORMAT   => "%s_idx",
                        Doctrine::ATTR_SEQNAME_FORMAT   => "%s_seq",
                        Doctrine::ATTR_QUOTE_IDENTIFIER => false,
                        Doctrine::ATTR_SEQCOL_NAME      => 'id',
                        Doctrine::ATTR_PORTABILITY      => Doctrine::PORTABILITY_ALL,
zYne's avatar
zYne committed
97
                        Doctrine::ATTR_EXPORT           => Doctrine::EXPORT_ALL,
98
                        Doctrine::ATTR_DECIMAL_PLACES   => 2,
lsmith's avatar
lsmith committed
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114
                        );
            foreach ($attributes as $attribute => $value) {
                $old = $this->getAttribute($attribute);
                if ($old === null) {
                    $this->setAttribute($attribute,$value);
                }
            }
            return true;
        }
        return false;
    }
    /**
     * returns the root directory of Doctrine
     *
     * @return string
     */
lsmith's avatar
lsmith committed
115 116
    final public function getRoot()
    {
117
        return $this->_root;
lsmith's avatar
lsmith committed
118 119 120 121 122 123 124 125
    }
    /**
     * getInstance
     * returns an instance of this class
     * (this class uses the singleton pattern)
     *
     * @return Doctrine_Manager
     */
lsmith's avatar
lsmith committed
126 127
    public static function getInstance()
    {
lsmith's avatar
lsmith committed
128 129 130 131 132 133
        static $instance;
        if ( ! isset($instance)) {
            $instance = new self();
        }
        return $instance;
    }
zYne's avatar
zYne committed
134 135 136 137 138 139 140 141 142 143

    /**
     * getQueryRegistry
     * lazy-initializes the query registry object and returns it
     *
     * @return Doctrine_Query_Registry
     */
    public function getQueryRegistry()
    {
    	if ( ! isset($this->_queryRegistry)) {
zYne's avatar
zYne committed
144
    	   $this->_queryRegistry = new Doctrine_Query_Registry;
zYne's avatar
zYne committed
145 146 147
    	}
        return $this->_queryRegistry;
    }
zYne's avatar
zYne committed
148 149 150 151 152 153 154
    
    /**
     * setQueryRegistry
     * sets the query registry
     *
     * @return Doctrine_Manager     this object
     */
zYne's avatar
zYne committed
155
    public function setQueryRegistry(Doctrine_Query_Registry $registry)
zYne's avatar
zYne committed
156 157 158 159 160
    {
        $this->_queryRegistry = $registry;
        
        return $this;
    }
zYne's avatar
zYne committed
161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198
    /**
     * fetch
     * fetches data using the provided queryKey and 
     * the associated query in the query registry
     *
     * if no query for given queryKey is being found a 
     * Doctrine_Query_Registry exception is being thrown
     *
     * @param string $queryKey      the query key
     * @param array $params         prepared statement params (if any)
     * @return mixed                the fetched data
     */
    public function find($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
    {
        return Doctrine_Manager::getInstance()
                            ->getQueryRegistry()
                            ->get($queryKey)
                            ->execute($params, $hydrationMode);
    }
    /**
     * fetchOne
     * fetches data using the provided queryKey and 
     * the associated query in the query registry
     *
     * if no query for given queryKey is being found a 
     * Doctrine_Query_Registry exception is being thrown
     *
     * @param string $queryKey      the query key
     * @param array $params         prepared statement params (if any)
     * @return mixed                the fetched data
     */
    public function findOne($queryKey, $params = array(), $hydrationMode = Doctrine::HYDRATE_RECORD)
    {
        return Doctrine_Manager::getInstance()
                            ->getQueryRegistry()
                            ->get($queryKey)
                            ->fetchOne($params, $hydrationMode);
    }
lsmith's avatar
lsmith committed
199 200
    /**
     * connection
lsmith's avatar
lsmith committed
201
     *
lsmith's avatar
lsmith committed
202 203 204
     * if the adapter parameter is set this method acts as
     * a short cut for Doctrine_Manager::getInstance()->openConnection($adapter, $name);
     *
lsmith's avatar
lsmith committed
205
     * if the adapter paramater is not set this method acts as
lsmith's avatar
lsmith committed
206 207 208 209 210 211 212
     * a short cut for Doctrine_Manager::getInstance()->getCurrentConnection()
     *
     * @param PDO|Doctrine_Adapter_Interface $adapter   database driver
     * @param string $name                              name of the connection, if empty numeric key is used
     * @throws Doctrine_Manager_Exception               if trying to bind a connection with an existing name
     * @return Doctrine_Connection
     */
lsmith's avatar
lsmith committed
213 214
    public static function connection($adapter = null, $name = null)
    {
lsmith's avatar
lsmith committed
215 216 217 218 219
        if ($adapter == null) {
            return Doctrine_Manager::getInstance()->getCurrentConnection();
        } else {
            return Doctrine_Manager::getInstance()->openConnection($adapter, $name);
        }
lsmith's avatar
lsmith committed
220
    }
lsmith's avatar
lsmith committed
221 222 223 224 225 226 227
    /**
     * openConnection
     * opens a new connection and saves it to Doctrine_Manager->connections
     *
     * @param PDO|Doctrine_Adapter_Interface $adapter   database driver
     * @param string $name                              name of the connection, if empty numeric key is used
     * @throws Doctrine_Manager_Exception               if trying to bind a connection with an existing name
zYne's avatar
zYne committed
228
     * @throws Doctrine_Manager_Exception               if trying to open connection for unknown driver
lsmith's avatar
lsmith committed
229 230
     * @return Doctrine_Connection
     */
231
    public function openConnection($adapter, $name = null, $setCurrent = true)
lsmith's avatar
lsmith committed
232
    {
233
        if (is_object($adapter)) {
234 235 236 237 238 239 240 241 242 243 244
            if ( ! ($adapter instanceof PDO) && ! in_array('Doctrine_Adapter_Interface', class_implements($adapter))) {
                throw new Doctrine_Manager_Exception("First argument should be an instance of PDO or implement Doctrine_Adapter_Interface");
            }

            $driverName = $adapter->getAttribute(Doctrine::ATTR_DRIVER_NAME);
        } elseif (is_array($adapter)) {
            if ( ! isset($adapter[0])) {
                throw new Doctrine_Manager_Exception('Empty data source name given.');
            }
            $e = explode(':', $adapter[0]);

245
            if ($e[0] == 'uri') {
246 247
                $e[0] = 'odbc';
            }
lsmith's avatar
lsmith committed
248

249 250 251 252 253 254 255 256 257 258 259 260 261
            $parts['dsn']    = $adapter[0];
            $parts['scheme'] = $e[0];
            $parts['user']   = (isset($adapter[1])) ? $adapter[1] : null;
            $parts['pass']   = (isset($adapter[2])) ? $adapter[2] : null;
            
            $driverName = $e[0];
            $adapter = $parts;
        } else {
            $parts = $this->parseDsn($adapter);
            
            $driverName = $parts['scheme'];
            
            $adapter = $parts;
262 263
        }

lsmith's avatar
lsmith committed
264 265 266 267 268
        // initialize the default attributes
        $this->setDefaultAttributes();

        if ($name !== null) {
            $name = (string) $name;
269 270
            if (isset($this->_connections[$name])) {
                return $this->_connections[$name];
lsmith's avatar
lsmith committed
271 272
            }
        } else {
273 274
            $name = $this->_index;
            $this->_index++;
lsmith's avatar
lsmith committed
275
        }
zYne's avatar
zYne committed
276

277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324
        $drivers = array('mysql'    => 'Doctrine_Connection_Mysql',
                         'sqlite'   => 'Doctrine_Connection_Sqlite',
                         'pgsql'    => 'Doctrine_Connection_Pgsql',
                         'oci'      => 'Doctrine_Connection_Oracle',
                         'oci8'     => 'Doctrine_Connection_Oracle',
                         'oracle'   => 'Doctrine_Connection_Oracle',
                         'mssql'    => 'Doctrine_Connection_Mssql',
                         'dblib'    => 'Doctrine_Connection_Mssql',
                         'firebird' => 'Doctrine_Connection_Firebird',
                         'informix' => 'Doctrine_Connection_Informix',
                         'mock'     => 'Doctrine_Connection_Mock');
        if ( ! isset($drivers[$driverName])) {
            throw new Doctrine_Manager_Exception('Unknown driver ' . $driverName);
        }
        $className = $drivers[$driverName];
        $conn = new $className($this, $adapter);

        $this->_connections[$name] = $conn;

        if ($setCurrent) {
            $this->_currIndex = $name;
        }
        return $this->_connections[$name];
    }
    /**
     * parseDsn
     *
     * @param string $dsn
     * @return array Parsed contents of DSN
     */
    public function parseDsn($dsn)
    {
        // silence any warnings
        $parts = @parse_url($dsn);

        $names = array('dsn', 'scheme', 'host', 'port', 'user', 'pass', 'path', 'query', 'fragment');

        foreach ($names as $name) {
            if ( ! isset($parts[$name])) {
                $parts[$name] = null;
            }
        }

        if (count($parts) == 0 || ! isset($parts['scheme'])) {
            throw new Doctrine_Manager_Exception('Empty data source name');
        }

        switch ($parts['scheme']) {
325
            case 'sqlite':
326 327 328 329 330
            case 'sqlite2':
            case 'sqlite3':
                if (isset($parts['host']) && $parts['host'] == ':memory') {
                    $parts['database'] = ':memory:';
                    $parts['dsn']      = 'sqlite::memory:';
zYne's avatar
zYne committed
331 332
                } else {
                    $parts['database'] = $parts['path'];
333
                    $parts['dsn'] = $parts['scheme'] . ':' . $parts['path'];
334 335
                }

336
                break;
337 338
            case 'mysql':
            case 'informix':
zYne's avatar
zYne committed
339
            case 'oci8':
340
            case 'oci':
341 342
            case 'mssql':
            case 'firebird':
343 344 345
            case 'dblib':
            case 'pgsql':
            case 'odbc':
346
            case 'mock':
347 348
            case 'oracle':
                if ( ! isset($parts['path']) || $parts['path'] == '/') {
zYne's avatar
zYne committed
349
                    throw new Doctrine_Manager_Exception('No database available in data source name');
350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369
                }
                if (isset($parts['path'])) {
                    $parts['database'] = substr($parts['path'], 1);
                }
                if ( ! isset($parts['host'])) {
                    throw new Doctrine_Manager_Exception('No hostname set in data source name');
                }
                
                if (isset(self::$driverMap[$parts['scheme']])) {
                    $parts['scheme'] = self::$driverMap[$parts['scheme']];
                }

                $parts['dsn'] = $parts['scheme'] . ':host='
                              . $parts['host'] . ';dbname='
                              . $parts['database'];
                
                if (isset($parts['port'])) {
                    // append port to dsn if supplied
                    $parts['dsn'] .= ';port=' . $parts['port'];
                }
370 371
                break;
            default:
372
                throw new Doctrine_Manager_Exception('Unknown driver '.$parts['scheme']);
373
        }
374 375 376


        return $parts;
lsmith's avatar
lsmith committed
377 378 379 380 381 382 383
    }
    /**
     * getConnection
     * @param integer $index
     * @return object Doctrine_Connection
     * @throws Doctrine_Manager_Exception   if trying to get a non-existent connection
     */
lsmith's avatar
lsmith committed
384 385
    public function getConnection($name)
    {
386
        if ( ! isset($this->_connections[$name])) {
lsmith's avatar
lsmith committed
387 388
            throw new Doctrine_Manager_Exception('Unknown connection: ' . $name);
        }
zYne's avatar
zYne committed
389

390
        return $this->_connections[$name];
lsmith's avatar
lsmith committed
391
    }
392
    /**
393
     * getComponentAlias
394
     * retrieves the alias for given component name
395 396 397 398 399 400 401 402 403 404 405
     * if the alias couldn't be found, this method returns the given
     * component name
     *
     * @param string $componentName
     * @return string                   the component alias
     */
    public function getComponentAlias($componentName)
    {
        if (isset($this->componentAliases[$componentName])) {
            return $this->componentAliases[$componentName];
        }
406

407 408 409 410 411 412 413 414 415 416 417
        return $componentName;
    }
    /**
     * sets an alias for given component name
     * very useful when building a large framework with a possibility
     * to override any given class
     *
     * @param string $componentName         the name of the component
     * @param string $alias
     * @return Doctrine_Manager
     */
418
    public function setComponentAlias($componentName, $alias)
419 420
    {
        $this->componentAliases[$componentName] = $alias;
421

422 423
        return $this;
    }
zYne's avatar
zYne committed
424 425 426
    /**
     * getConnectionName
     *
zYne's avatar
zYne committed
427 428
     * @param Doctrine_Connection $conn     connection object to be searched for
     * @return string                       the name of the connection
zYne's avatar
zYne committed
429
     */
430
    public function getConnectionName(Doctrine_Connection $conn)
zYne's avatar
zYne committed
431
    {
432
        return array_search($conn, $this->_connections, true);
zYne's avatar
zYne committed
433
    }
lsmith's avatar
lsmith committed
434 435 436 437 438 439 440 441 442 443
    /**
     * bindComponent
     * binds given component to given connection
     * this means that when ever the given component uses a connection
     * it will be using the bound connection instead of the current connection
     *
     * @param string $componentName
     * @param string $connectionName
     * @return boolean
     */
lsmith's avatar
lsmith committed
444 445
    public function bindComponent($componentName, $connectionName)
    {
446
        $this->_bound[$componentName] = $connectionName;
lsmith's avatar
lsmith committed
447 448 449 450 451 452 453
    }
    /**
     * getConnectionForComponent
     *
     * @param string $componentName
     * @return Doctrine_Connection
     */
lsmith's avatar
lsmith committed
454 455
    public function getConnectionForComponent($componentName = null)
    {
456 457
        if (isset($this->_bound[$componentName])) {
            return $this->getConnection($this->_bound[$componentName]);
lsmith's avatar
lsmith committed
458 459 460
        }
        return $this->getCurrentConnection();
    }
lsmith's avatar
lsmith committed
461
    /**
lsmith's avatar
lsmith committed
462 463 464 465 466 467 468 469
     * getTable
     * this is the same as Doctrine_Connection::getTable() except
     * that it works seamlessly in multi-server/connection environment
     *
     * @see Doctrine_Connection::getTable()
     * @param string $componentName
     * @return Doctrine_Table
     */
lsmith's avatar
lsmith committed
470 471
    public function getTable($componentName)
    {
lsmith's avatar
lsmith committed
472 473
        return $this->getConnectionForComponent($componentName)->getTable($componentName);
    }
zYne's avatar
zYne committed
474 475 476 477 478 479 480 481 482 483 484 485 486 487 488
    /**
     * table
     * this is the same as Doctrine_Connection::getTable() except
     * that it works seamlessly in multi-server/connection environment
     *
     * @see Doctrine_Connection::getTable()
     * @param string $componentName
     * @return Doctrine_Table
     */
    public static function table($componentName)
    {
        return Doctrine_Manager::getInstance()
               ->getConnectionForComponent($componentName)
               ->getTable($componentName);
    }
lsmith's avatar
lsmith committed
489 490 491 492 493 494
    /**
     * closes the connection
     *
     * @param Doctrine_Connection $connection
     * @return void
     */
lsmith's avatar
lsmith committed
495 496
    public function closeConnection(Doctrine_Connection $connection)
    {
lsmith's avatar
lsmith committed
497
        $connection->close();
498

499
        $key = array_search($connection, $this->_connections, true);
zYne's avatar
zYne committed
500 501

        if ($key !== false) {
502
            unset($this->_connections[$key]);
zYne's avatar
zYne committed
503
        }
504
        $this->_currIndex = key($this->_connections);
zYne's avatar
zYne committed
505

lsmith's avatar
lsmith committed
506 507 508 509 510 511 512 513
        unset($connection);
    }
    /**
     * getConnections
     * returns all opened connections
     *
     * @return array
     */
lsmith's avatar
lsmith committed
514 515
    public function getConnections()
    {
516
        return $this->_connections;
lsmith's avatar
lsmith committed
517 518 519 520 521 522 523 524 525
    }
    /**
     * setCurrentConnection
     * sets the current connection to $key
     *
     * @param mixed $key                        the connection key
     * @throws InvalidKeyException
     * @return void
     */
lsmith's avatar
lsmith committed
526 527
    public function setCurrentConnection($key)
    {
lsmith's avatar
lsmith committed
528
        $key = (string) $key;
529
        if ( ! isset($this->_connections[$key])) {
lsmith's avatar
lsmith committed
530 531
            throw new InvalidKeyException();
        }
532
        $this->_currIndex = $key;
lsmith's avatar
lsmith committed
533
    }
zYne's avatar
zYne committed
534 535 536 537 538 539 540
    /**
     * contains
     * whether or not the manager contains specified connection
     *
     * @param mixed $key                        the connection key
     * @return boolean
     */
541
    public function contains($key)
zYne's avatar
zYne committed
542
    {
543
        return isset($this->_connections[$key]);
zYne's avatar
zYne committed
544
    }
lsmith's avatar
lsmith committed
545 546 547 548 549 550
    /**
     * count
     * returns the number of opened connections
     *
     * @return integer
     */
lsmith's avatar
lsmith committed
551 552
    public function count()
    {
553
        return count($this->_connections);
lsmith's avatar
lsmith committed
554 555 556 557 558 559 560
    }
    /**
     * getIterator
     * returns an ArrayIterator that iterates through all connections
     *
     * @return ArrayIterator
     */
lsmith's avatar
lsmith committed
561 562
    public function getIterator()
    {
563
        return new ArrayIterator($this->_connections);
lsmith's avatar
lsmith committed
564 565 566 567 568 569 570 571
    }
    /**
     * getCurrentConnection
     * returns the current connection
     *
     * @throws Doctrine_Connection_Exception       if there are no open connections
     * @return Doctrine_Connection
     */
lsmith's avatar
lsmith committed
572 573
    public function getCurrentConnection()
    {
574 575
        $i = $this->_currIndex;
        if ( ! isset($this->_connections[$i])) {
lsmith's avatar
lsmith committed
576 577
            throw new Doctrine_Connection_Exception();
        }
578
        return $this->_connections[$i];
lsmith's avatar
lsmith committed
579 580 581 582 583 584 585
    }
    /**
     * __toString
     * returns a string representation of this object
     *
     * @return string
     */
lsmith's avatar
lsmith committed
586 587
    public function __toString()
    {
lsmith's avatar
lsmith committed
588 589
        $r[] = "<pre>";
        $r[] = "Doctrine_Manager";
590
        $r[] = "Connections : ".count($this->_connections);
lsmith's avatar
lsmith committed
591 592 593
        $r[] = "</pre>";
        return implode("\n",$r);
    }
594
}