Hydrate.php 19.3 KB
Newer Older
1
<?php
lsmith's avatar
lsmith committed
2
/*
doctrine's avatar
doctrine committed
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>.
 */
zYne's avatar
zYne committed
21

doctrine's avatar
doctrine committed
22 23 24 25 26
/**
 * Doctrine_Hydrate is a base class for Doctrine_RawSql and Doctrine_Query.
 * Its purpose is to populate object graphs.
 *
 *
27 28 29 30 31 32 33 34
 * @package     Doctrine
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
 * @category    Object Relational Mapping
 * @link        www.phpdoctrine.com
 * @since       1.0
 * @version     $Revision$
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
 */
zYne's avatar
zYne committed
35
class Doctrine_Hydrate
lsmith's avatar
lsmith committed
36
{
37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
    /**
     * QUERY TYPE CONSTANTS
     */

    /**
     * constant for SELECT queries
     */
    const SELECT = 0;
    /**
     * constant for DELETE queries
     */
    const DELETE = 1;
    /**
     * constant for UPDATE queries
     */
    const UPDATE = 2;
    /**
     * constant for INSERT queries
     */
    const INSERT = 3;
    /**
     * constant for CREATE queries
     */
    const CREATE = 4;
zYne's avatar
zYne committed
61

62 63 64
    /**
     * @var array $params                       query input parameters
     */
zYne's avatar
zYne committed
65
    protected $params      = array();
66
    /**
67
     * @var Doctrine_Connection $conn           Doctrine_Connection object
68
     */
69
    protected $conn;
70
    /**
zYne's avatar
zYne committed
71 72
     * @var Doctrine_View $_view                Doctrine_View object, when set this object will use the
     *                                          the query given by the view object for object population
73
     */
zYne's avatar
zYne committed
74
    protected $_view;
75
    /**
zYne's avatar
zYne committed
76 77 78 79 80 81 82 83
     * @var array $_aliasMap                    two dimensional array containing the map for query aliases
     *      Main keys are component aliases
     *
     *          table               table object associated with given alias
     *
     *          relation            the relation object owned by the parent
     *
     *          parent              the alias of the parent
84
     */
zYne's avatar
zYne committed
85
    protected $_aliasMap        = array();
86
    /**
zYne's avatar
zYne committed
87
     *
88
     */
zYne's avatar
zYne committed
89
    protected $pendingAggregates = array();
90
    /**
zYne's avatar
zYne committed
91 92
     *
     */
zYne's avatar
zYne committed
93
    protected $subqueryAggregates = array();
94 95 96 97
    /**
     * @var array $aggregateMap             an array containing all aggregate aliases, keys as dql aliases
     *                                      and values as sql aliases
     */
zYne's avatar
zYne committed
98
    protected $aggregateMap      = array();
99
    /**
zYne's avatar
zYne committed
100 101
     * @var Doctrine_Hydrate_Alias $aliasHandler    handles the creation and storage of table aliases and
     *                                              binds the aliases to component aliases / paths
102
     */
103
    protected $aliasHandler;
104 105 106 107
    /**
     * @var array $parts            SQL query string parts
     */
    protected $parts = array(
zYne's avatar
zYne committed
108
        'select'    => array(),
zYne's avatar
zYne committed
109 110
        'distinct'  => false,
        'forUpdate' => false,
zYne's avatar
zYne committed
111 112 113 114 115 116 117 118 119
        'from'      => array(),
        'set'       => array(),
        'join'      => array(),
        'where'     => array(),
        'groupby'   => array(),
        'having'    => array(),
        'orderby'   => array(),
        'limit'     => false,
        'offset'    => false,
120
        );
121 122 123 124 125 126
    /**
     * @var integer $type                   the query type
     *
     * @see Doctrine_Query::* constants
     */
    protected $type            = self::SELECT;
127 128 129
    /**
     * constructor
     *
130
     * @param Doctrine_Connection|null $connection
131
     */
lsmith's avatar
lsmith committed
132 133
    public function __construct($connection = null)
    {
lsmith's avatar
lsmith committed
134
        if ( ! ($connection instanceof Doctrine_Connection)) {
135
            $connection = Doctrine_Manager::getInstance()->getCurrentConnection();
lsmith's avatar
lsmith committed
136
        }
137
        $this->conn = $connection;
138
        $this->aliasHandler = new Doctrine_Hydrate_Alias();
139
    }
zYne's avatar
zYne committed
140

141
    public function getTableAlias($componentAlias)
zYne's avatar
zYne committed
142 143 144
    {
        return $this->aliasHandler->getShortAlias($componentAlias);
    }
145
    public function addQueryPart($name, $part)
zYne's avatar
zYne committed
146 147 148 149 150 151 152
    {
        if ( ! isset($this->parts[$name])) {
            throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name);
        }
        $this->parts[$name][] = $part;
    }
    public function getDeclaration($name)
lsmith's avatar
lsmith committed
153
    {
154
        if ( ! isset($this->_aliasMap[$name])) {
zYne's avatar
zYne committed
155
            throw new Doctrine_Hydrate_Exception('Unknown component alias ' . $name);
156 157 158
        }

        return $this->_aliasMap[$name];
zYne's avatar
zYne committed
159 160 161 162 163
    }
    public function setQueryPart($name, $part)
    {
        if ( ! isset($this->parts[$name])) {
            throw new Doctrine_Hydrate_Exception('Unknown query part ' . $name);
164
        }
zYne's avatar
zYne committed
165 166 167 168 169 170

        if ($name !== 'limit' && $name !== 'offset') {
            $this->parts[$name] = array($part);
        } else {
            $this->parts[$name] = $part;
        }
171
    }
172 173 174 175 176
    /**
     * copyAliases
     *
     * @return void
     */
zYne's avatar
zYne committed
177
    public function copyAliases($query)
lsmith's avatar
lsmith committed
178
    {
pookey's avatar
pookey committed
179
        $this->aliasHandler = $query->aliasHandler;
lsmith's avatar
lsmith committed
180

181
        return $this;
182
    }
183 184
    /**
     * createSubquery
lsmith's avatar
lsmith committed
185
     *
186 187
     * @return Doctrine_Hydrate
     */
lsmith's avatar
lsmith committed
188 189
    public function createSubquery()
    {
190 191
        $class = get_class($this);
        $obj   = new $class();
lsmith's avatar
lsmith committed
192

193 194
        // copy the aliases to the subquery
        $obj->copyAliases($this);
195

pookey's avatar
pookey committed
196 197 198
        // this prevents the 'id' being selected, re ticket #307
        $obj->isSubquery(true);

199 200
        return $obj;
    }
201 202
    /**
     * limitSubqueryUsed
zYne's avatar
zYne committed
203 204
     *
     * @return boolean
205
     */
lsmith's avatar
lsmith committed
206 207
    public function isLimitSubqueryUsed()
    {
208 209
        return false;
    }
210
    public function getQueryPart($part)
zYne's avatar
zYne committed
211 212 213 214
    {
        if ( ! isset($this->parts[$part])) {
            throw new Doctrine_Hydrate_Exception('Unknown query part ' . $part);
        }
215

zYne's avatar
zYne committed
216 217
        return $this->parts[$part];
    }
218 219 220 221 222
    /**
     * remove
     *
     * @param $name
     */
lsmith's avatar
lsmith committed
223 224
    public function remove($name)
    {
lsmith's avatar
lsmith committed
225 226 227 228 229 230 231 232 233
        if (isset($this->parts[$name])) {
            if ($name == "limit" || $name == "offset") {
                $this->parts[$name] = false;
            } else {
                $this->parts[$name] = array();
            }
        }
        return $this;
    }
234 235 236
    /**
     * clear
     * resets all the variables
lsmith's avatar
lsmith committed
237
     *
238 239
     * @return void
     */
lsmith's avatar
lsmith committed
240 241
    protected function clear()
    {
242
        $this->parts = array(
zYne's avatar
zYne committed
243
                    'select'    => array(),
zYne's avatar
zYne committed
244 245
                    'distinct'  => false,
                    'forUpdate' => false,
zYne's avatar
zYne committed
246 247 248 249 250 251 252 253 254 255
                    'from'      => array(),
                    'set'       => array(),
                    'join'      => array(),
                    'where'     => array(),
                    'groupby'   => array(),
                    'having'    => array(),
                    'orderby'   => array(),
                    'limit'     => false,
                    'offset'    => false,
                    );
256
        $this->inheritanceApplied = false;
257
        $this->aliasHandler->clear();
258 259
    }
    /**
zYne's avatar
zYne committed
260 261
     * getConnection
     *
zYne's avatar
zYne committed
262
     * @return Doctrine_Connection
263
     */
lsmith's avatar
lsmith committed
264 265
    public function getConnection()
    {
266
        return $this->conn;
267 268 269 270 271 272 273 274 275
    }
    /**
     * setView
     * sets a database view this query object uses
     * this method should only be called internally by doctrine
     *
     * @param Doctrine_View $view       database view
     * @return void
     */
lsmith's avatar
lsmith committed
276 277
    public function setView(Doctrine_View $view)
    {
zYne's avatar
zYne committed
278
        $this->_view = $view;
279 280 281
    }
    /**
     * getView
282
     * returns the view associated with this query object (if any)
283
     *
284
     * @return Doctrine_View        the view associated with this query object
285
     */
lsmith's avatar
lsmith committed
286 287
    public function getView()
    {
zYne's avatar
zYne committed
288
        return $this->_view;
289
    }
zYne's avatar
zYne committed
290 291
    /**
     * getParams
lsmith's avatar
lsmith committed
292
     *
zYne's avatar
zYne committed
293 294
     * @return array
     */
lsmith's avatar
lsmith committed
295 296
    public function getParams()
    {
297
        return $this->params;
zYne's avatar
zYne committed
298
    }
zYne's avatar
zYne committed
299 300 301 302 303 304 305 306
    /**
     * setParams
     *
     * @param array $params
     */
    public function setParams(array $params = array()) {
        $this->params = $params;
    }
zYne's avatar
zYne committed
307 308 309 310
    public function convertEnums($params)
    {
        return $params;
    }
311
    public function setAliasMap($map)
zYne's avatar
zYne committed
312 313 314
    {
        $this->_aliasMap = $map;
    }
zYne's avatar
zYne committed
315
    public function getAliasMap()
zYne's avatar
zYne committed
316 317 318 319 320 321 322 323 324 325 326 327 328
    {
        return $this->_aliasMap;
    }
    /**
     * mapAggregateValues
     * map the aggregate values of given dataset row to a given record
     *
     * @param Doctrine_Record $record
     * @param array $row
     * @return Doctrine_Record
     */
    public function mapAggregateValues($record, array $row, $alias)
    {
329
        $found = false;
zYne's avatar
zYne committed
330 331 332 333 334 335 336 337 338 339 340 341 342 343
        // aggregate values have numeric keys
        if (isset($row[0])) {
            // map each aggregate value
            foreach ($row as $index => $value) {
                $agg = false;

                if (isset($this->pendingAggregates[$alias][$index])) {
                    $agg = $this->pendingAggregates[$alias][$index][3];
                } elseif (isset($this->subqueryAggregates[$alias][$index])) {
                    $agg = $this->subqueryAggregates[$alias][$index];
                }
                $record->mapValue($agg, $value);
                $found = true;
            }
zYne's avatar
zYne committed
344
        }
zYne's avatar
zYne committed
345 346 347 348 349 350 351 352 353
        return $found;
    }
    /**
     * execute
     * executes the dql query and populates all collections
     *
     * @param string $params
     * @return Doctrine_Collection            the root collection
     */
354
    public function execute($params = array(), $return = Doctrine::FETCH_RECORD)
zYne's avatar
zYne committed
355
    {
356 357 358 359 360 361 362 363
        $params = $this->conn->convertBooleans(array_merge($this->params, $params));
        $params = $this->convertEnums($params);

        if ( ! $this->_view) {
            $query = $this->getQuery($params);
        } else {
            $query = $this->_view->getSelectSql();
        }
zYne's avatar
zYne committed
364

365 366 367 368 369 370 371 372 373 374 375 376
        if ($this->isLimitSubqueryUsed() &&
            $this->conn->getDBH()->getAttribute(Doctrine::ATTR_DRIVER_NAME) !== 'mysql') {

            $params = array_merge($params, $params);
        }

        if ($this->type !== self::SELECT) {
            return $this->conn->exec($query, $params);
        }

        $stmt  = $this->conn->execute($query, $params);
        $array = (array) $this->parseData($stmt);
zYne's avatar
zYne committed
377

zYne's avatar
zYne committed
378 379
        if (empty($this->_aliasMap)) {
            throw new Doctrine_Hydrate_Exception("Couldn't execute query. Component alias map was empty.");
zYne's avatar
zYne committed
380
        }
zYne's avatar
zYne committed
381 382 383 384
        // initialize some variables used within the main loop
        reset($this->_aliasMap);
        $rootMap     = current($this->_aliasMap);
        $rootAlias   = key($this->_aliasMap);
zYne's avatar
zYne committed
385
        $coll        = new Doctrine_Collection($rootMap['table']);
zYne's avatar
zYne committed
386
        $prev[$rootAlias] = $coll;
zYne's avatar
zYne committed
387

zYne's avatar
zYne committed
388 389
        // we keep track of all the collections
        $colls   = array();
zYne's avatar
zYne committed
390
        $colls[] = $coll;
zYne's avatar
zYne committed
391 392 393 394 395
        $prevRow = array();
        /**
         * iterate over the fetched data
         * here $data is a two dimensional array
         */
lsmith's avatar
lsmith committed
396
        foreach ($array as $data) {
397 398 399
            /**
             * remove duplicated data rows and map data into objects
             */
zYne's avatar
zYne committed
400 401
            foreach ($data as $tableAlias => $row) {
                // skip empty rows (not mappable)
lsmith's avatar
lsmith committed
402
                if (empty($row)) {
403
                    continue;
lsmith's avatar
lsmith committed
404
                }
zYne's avatar
zYne committed
405 406
                $alias = $this->aliasHandler->getComponentAlias($tableAlias);
                $map   = $this->_aliasMap[$alias];
407

zYne's avatar
zYne committed
408 409 410
                // initialize previous row array if not set
                if ( ! isset($prevRow[$tableAlias])) {
                    $prevRow[$tableAlias] = array();
zYne's avatar
zYne committed
411
                }
zYne's avatar
zYne committed
412

zYne's avatar
zYne committed
413 414 415
                // don't map duplicate rows
                if ($prevRow[$tableAlias] !== $row) {
                    $identifiable = $this->isIdentifiable($row, $map['table']->getIdentifier());
416

zYne's avatar
zYne committed
417 418 419
                    if ($identifiable) {
                        // set internal data
                        $map['table']->setData($row);
zYne's avatar
zYne committed
420
                    }
zYne's avatar
zYne committed
421

zYne's avatar
zYne committed
422 423
                    // initialize a new record
                    $record = $map['table']->getRecord();
zYne's avatar
zYne committed
424

zYne's avatar
zYne committed
425 426 427
                    // map aggregate values (if any)
                    if($this->mapAggregateValues($record, $row, $alias)) {
                        $identifiable = true;
zYne's avatar
zYne committed
428 429
                    }

430

zYne's avatar
zYne committed
431 432
                    if ($alias == $rootAlias) {
                        // add record into root collection
zYne's avatar
zYne committed
433

zYne's avatar
zYne committed
434 435 436 437 438
                        if ($identifiable) {
                            $coll->add($record);
                            unset($prevRow);
                        }
                    } else {
zYne's avatar
zYne committed
439

zYne's avatar
zYne committed
440 441 442 443
                        $relation    = $map['relation'];
                        $parentAlias = $map['parent'];
                        $parentMap   = $this->_aliasMap[$parentAlias];
                        $parent      = $prev[$parentAlias]->getLast();
444

zYne's avatar
zYne committed
445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460
                        // check the type of the relation
                        if ($relation->isOneToOne()) {
                            if ( ! $identifiable) {
                                continue;
                            }
                            $prev[$alias] = $record;
                        } else {
                            // one-to-many relation or many-to-many relation
                            if ( ! $prev[$parentAlias]->getLast()->hasReference($relation->getAlias())) {
                                // initialize a new collection
                                $prev[$alias] = new Doctrine_Collection($map['table']);
                                $prev[$alias]->setReference($parent, $relation);
                            } else {
                                // previous entry found from memory
                                $prev[$alias] = $prev[$parentAlias]->getLast()->get($relation->getAlias());
                            }
461

zYne's avatar
zYne committed
462 463
                            $colls[] = $prev[$alias];

zYne's avatar
zYne committed
464 465 466
                            // add record to the current collection
                            if ($identifiable) {
                                $prev[$alias]->add($record);
lsmith's avatar
lsmith committed
467
                            }
zYne's avatar
zYne committed
468
                        }
zYne's avatar
zYne committed
469 470
                        // initialize the relation from parent to the current collection/record
                        $parent->set($relation->getAlias(), $prev[$alias]);
zYne's avatar
zYne committed
471
                    }
472

473 474 475
                    // following statement is needed to ensure that mappings
                    // are being done properly when the result set doesn't
                    // contain the rows in 'right order'
476

zYne's avatar
zYne committed
477 478
                    if ($prev[$alias] !== $record) {
                        $prev[$alias] = $record;
zYne's avatar
zYne committed
479
                    }
480
                }
zYne's avatar
zYne committed
481
                $prevRow[$tableAlias] = $row;
482 483
            }
        }
zYne's avatar
zYne committed
484
        // take snapshots from all initialized collections
zYne's avatar
zYne committed
485 486
        foreach(array_unique($colls) as $c) {
            $c->takeSnapshot();
zYne's avatar
zYne committed
487 488
        }

489
        return $coll;
490
    }
491
    /**
492
     * isIdentifiable
lsmith's avatar
lsmith committed
493
     * returns whether or not a given data row is identifiable (it contains
zYne's avatar
zYne committed
494
     * all primary key fields specified in the second argument)
495 496
     *
     * @param array $row
zYne's avatar
zYne committed
497
     * @param mixed $primaryKeys
498 499
     * @return boolean
     */
zYne's avatar
zYne committed
500
    public function isIdentifiable(array $row, $primaryKeys)
lsmith's avatar
lsmith committed
501
    {
zYne's avatar
zYne committed
502 503 504 505 506
        if (is_array($primaryKeys)) {
            foreach ($primaryKeys as $id) {
                if ($row[$id] == null) {
                    return false;
                }
507 508
            }
        } else {
zYne's avatar
zYne committed
509 510
            if ( ! isset($row[$primaryKeys])) {
                return false;
lsmith's avatar
lsmith committed
511
            }
512
        }
zYne's avatar
zYne committed
513
        return true;
514
    }
515 516 517 518 519 520 521 522 523 524 525 526 527 528
    /**
     * getType
     *
     * returns the type of this query object
     * by default the type is Doctrine_Hydrate::SELECT but if update() or delete()
     * are being called the type is Doctrine_Hydrate::UPDATE and Doctrine_Hydrate::DELETE,
     * respectively
     *
     * @see Doctrine_Hydrate::SELECT
     * @see Doctrine_Hydrate::UPDATE
     * @see Doctrine_Hydrate::DELETE
     *
     * @return integer      return the query type
     */
529
    public function getType()
530 531 532
    {
        return $this->type;
    }
533 534
    /**
     * applyInheritance
535
     * applies column aggregation inheritance to DQL / SQL query
536 537 538
     *
     * @return string
     */
lsmith's avatar
lsmith committed
539 540
    public function applyInheritance()
    {
541 542 543
        // get the inheritance maps
        $array = array();

zYne's avatar
zYne committed
544 545 546
        foreach ($this->_aliasMap as $componentAlias => $data) {
            $tableAlias = $this->getTableAlias($componentAlias);
            $array[$tableAlias][] = $data['table']->inheritanceMap;
zYne's avatar
zYne committed
547
        }
548 549

        // apply inheritance maps
zYne's avatar
zYne committed
550
        $str = '';
551 552 553
        $c = array();

        $index = 0;
lsmith's avatar
lsmith committed
554
        foreach ($array as $tableAlias => $maps) {
555
            $a = array();
556

557 558 559 560 561 562
            // don't use table aliases if the query isn't a select query
            if ($this->type !== Doctrine_Query::SELECT) {
                $tableAlias = '';
            } else {
                $tableAlias .= '.';
            }
zYne's avatar
zYne committed
563

lsmith's avatar
lsmith committed
564
            foreach ($maps as $map) {
565
                $b = array();
lsmith's avatar
lsmith committed
566 567
                foreach ($map as $field => $value) {
                    if ($index > 0) {
568 569
                        $b[] = '(' . $tableAlias  . $field . ' = ' . $value
                             . ' OR ' . $tableAlias . $field . ' IS NULL)';
lsmith's avatar
lsmith committed
570
                    } else {
571
                        $b[] = $tableAlias . $field . ' = ' . $value;
lsmith's avatar
lsmith committed
572
                    }
573
                }
lsmith's avatar
lsmith committed
574 575

                if ( ! empty($b)) {
576
                    $a[] = implode(' AND ', $b);
lsmith's avatar
lsmith committed
577
                }
578
            }
579

lsmith's avatar
lsmith committed
580
            if ( ! empty($a)) {
581
                $c[] = implode(' AND ', $a);
lsmith's avatar
lsmith committed
582
            }
583 584 585
            $index++;
        }

586
        $str .= implode(' AND ', $c);
587 588 589 590 591

        return $str;
    }
    /**
     * parseData
592
     * parses the data returned by statement object
593
     *
594
     * @param mixed $stmt
595 596
     * @return array
     */
597
    public function parseData($stmt)
lsmith's avatar
lsmith committed
598
    {
599
        $array = array();
lsmith's avatar
lsmith committed
600 601

        while ($data = $stmt->fetch(PDO::FETCH_ASSOC)) {
602 603 604
            /**
             * parse the data into two-dimensional array
             */
lsmith's avatar
lsmith committed
605
            foreach ($data as $key => $value) {
606
                $e = explode('__', $key);
607

zYne's avatar
zYne committed
608 609
                $field      = strtolower(array_pop($e));
                $tableAlias = strtolower(implode('__', $e));
zYne's avatar
zYne committed
610

zYne's avatar
zYne committed
611
                $data[$tableAlias][$field] = $value;
612

613
                unset($data[$key]);
zYne's avatar
zYne committed
614
            }
615
            $array[] = $data;
zYne's avatar
zYne committed
616
        }
617

618 619 620
        $stmt->closeCursor();
        return $array;
    }
621 622 623
    /**
     * @return string                   returns a string representation of this object
     */
lsmith's avatar
lsmith committed
624 625
    public function __toString()
    {
626 627
        return Doctrine_Lib::formatSql($this->getQuery());
    }
628
}