| 1 | <?php |
|---|
| 2 | /* |
|---|
| 3 | * $Id: $ |
|---|
| 4 | * |
|---|
| 5 | * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|---|
| 6 | * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|---|
| 7 | * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|---|
| 8 | * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|---|
| 9 | * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|---|
| 10 | * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|---|
| 11 | * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|---|
| 12 | * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|---|
| 13 | * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|---|
| 14 | * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|---|
| 15 | * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|---|
| 16 | * |
|---|
| 17 | * This software consists of voluntary contributions made by many individuals |
|---|
| 18 | * and is licensed under the LGPL. For more information, see |
|---|
| 19 | * <http://www.phpdoctrine.org>. |
|---|
| 20 | */ |
|---|
| 21 | |
|---|
| 22 | /** |
|---|
| 23 | * Doctrine_Query |
|---|
| 24 | * A Doctrine_Query object represents a DQL query. It is used to query databases for |
|---|
| 25 | * data in an object-oriented fashion. A DQL query understands relations and inheritance |
|---|
| 26 | * and is dbms independant. |
|---|
| 27 | * |
|---|
| 28 | * @package Doctrine |
|---|
| 29 | * @subpackage Query |
|---|
| 30 | * @license http://www.opensource.org/licenses/lgpl-license.php LGPL |
|---|
| 31 | * @link www.phpdoctrine.org |
|---|
| 32 | * @since 1.0 |
|---|
| 33 | * @version $Revision: 4645 $ |
|---|
| 34 | * @author Konsta Vesterinen <kvesteri@cc.hut.fi> |
|---|
| 35 | * @todo Proposal: This class does far too much. It should have only 1 task: Collecting |
|---|
| 36 | * the DQL query parts and the query parameters (the query state and caching options/methods |
|---|
| 37 | * can remain here, too). |
|---|
| 38 | * The actual SQL construction could be done by a separate object (Doctrine_Query_SqlBuilder?) |
|---|
| 39 | * whose task it is to convert DQL into SQL. |
|---|
| 40 | * Furthermore the SqlBuilder? can then use other objects (Doctrine_Query_Tokenizer?), |
|---|
| 41 | * (Doctrine_Query_Parser(s)?) to accomplish his work. Doctrine_Query does not need |
|---|
| 42 | * to know the tokenizer/parsers. There could be extending |
|---|
| 43 | * implementations of SqlBuilder? that cover the specific SQL dialects. |
|---|
| 44 | * This would release Doctrine_Connection and the Doctrine_Connection_xxx classes |
|---|
| 45 | * from this tedious task. |
|---|
| 46 | * This would also largely reduce the currently huge interface of Doctrine_Query(_Abstract) |
|---|
| 47 | * and better hide all these transformation internals from the public Query API. |
|---|
| 48 | * |
|---|
| 49 | * @internal The lifecycle of a Query object is the following: |
|---|
| 50 | * After construction the query object is empty. Through using the fluent |
|---|
| 51 | * query interface the user fills the query object with DQL parts and query parameters. |
|---|
| 52 | * These get collected in {@link $_dqlParts} and {@link $_params}, respectively. |
|---|
| 53 | * When the query is executed the first time, or when {@link getSqlQuery()} |
|---|
| 54 | * is called the first time, the collected DQL parts get parsed and the resulting |
|---|
| 55 | * connection-driver specific SQL is generated. The generated SQL parts are |
|---|
| 56 | * stored in {@link $_sqlParts} and the final resulting SQL query is stored in |
|---|
| 57 | * {@link $_sql}. |
|---|
| 58 | */ |
|---|
| 59 | class Doctrine_Query extends Doctrine_Query_Abstract implements Countable, Serializable |
|---|
| 60 | { |
|---|
| 61 | /** |
|---|
| 62 | * @var array The DQL keywords. |
|---|
| 63 | */ |
|---|
| 64 | protected static $_keywords = array('ALL', |
|---|
| 65 | 'AND', |
|---|
| 66 | 'ANY', |
|---|
| 67 | 'AS', |
|---|
| 68 | 'ASC', |
|---|
| 69 | 'AVG', |
|---|
| 70 | 'BETWEEN', |
|---|
| 71 | 'BIT_LENGTH', |
|---|
| 72 | 'BY', |
|---|
| 73 | 'CHARACTER_LENGTH', |
|---|
| 74 | 'CHAR_LENGTH', |
|---|
| 75 | 'CURRENT_DATE', |
|---|
| 76 | 'CURRENT_TIME', |
|---|
| 77 | 'CURRENT_TIMESTAMP', |
|---|
| 78 | 'DELETE', |
|---|
| 79 | 'DESC', |
|---|
| 80 | 'DISTINCT', |
|---|
| 81 | 'EMPTY', |
|---|
| 82 | 'EXISTS', |
|---|
| 83 | 'FALSE', |
|---|
| 84 | 'FETCH', |
|---|
| 85 | 'FROM', |
|---|
| 86 | 'GROUP', |
|---|
| 87 | 'HAVING', |
|---|
| 88 | 'IN', |
|---|
| 89 | 'INDEXBY', |
|---|
| 90 | 'INNER', |
|---|
| 91 | 'IS', |
|---|
| 92 | 'JOIN', |
|---|
| 93 | 'LEFT', |
|---|
| 94 | 'LIKE', |
|---|
| 95 | 'LOWER', |
|---|
| 96 | 'MEMBER', |
|---|
| 97 | 'MOD', |
|---|
| 98 | 'NEW', |
|---|
| 99 | 'NOT', |
|---|
| 100 | 'NULL', |
|---|
| 101 | 'OBJECT', |
|---|
| 102 | 'OF', |
|---|
| 103 | 'OR', |
|---|
| 104 | 'ORDER', |
|---|
| 105 | 'OUTER', |
|---|
| 106 | 'POSITION', |
|---|
| 107 | 'SELECT', |
|---|
| 108 | 'SOME', |
|---|
| 109 | 'TRIM', |
|---|
| 110 | 'TRUE', |
|---|
| 111 | 'UNKNOWN', |
|---|
| 112 | 'UPDATE', |
|---|
| 113 | 'WHERE'); |
|---|
| 114 | |
|---|
| 115 | /** |
|---|
| 116 | * @var array |
|---|
| 117 | */ |
|---|
| 118 | protected $_subqueryAliases = array(); |
|---|
| 119 | |
|---|
| 120 | /** |
|---|
| 121 | * @var array $_aggregateAliasMap an array containing all aggregate aliases, keys as dql aliases |
|---|
| 122 | * and values as sql aliases |
|---|
| 123 | */ |
|---|
| 124 | protected $_aggregateAliasMap = array(); |
|---|
| 125 | |
|---|
| 126 | /** |
|---|
| 127 | * @var array |
|---|
| 128 | */ |
|---|
| 129 | protected $_pendingAggregates = array(); |
|---|
| 130 | |
|---|
| 131 | /** |
|---|
| 132 | * @param boolean $needsSubquery |
|---|
| 133 | */ |
|---|
| 134 | protected $_needsSubquery = false; |
|---|
| 135 | |
|---|
| 136 | /** |
|---|
| 137 | * @param boolean $isSubquery whether or not this query object is a subquery of another |
|---|
| 138 | * query object |
|---|
| 139 | */ |
|---|
| 140 | protected $_isSubquery; |
|---|
| 141 | |
|---|
| 142 | /** |
|---|
| 143 | * @var array $_neededTables an array containing the needed table aliases |
|---|
| 144 | */ |
|---|
| 145 | protected $_neededTables = array(); |
|---|
| 146 | |
|---|
| 147 | /** |
|---|
| 148 | * @var array $pendingSubqueries SELECT part subqueries, these are called pending subqueries since |
|---|
| 149 | * they cannot be parsed directly (some queries might be correlated) |
|---|
| 150 | */ |
|---|
| 151 | protected $_pendingSubqueries = array(); |
|---|
| 152 | |
|---|
| 153 | /** |
|---|
| 154 | * @var array $_pendingFields an array of pending fields (fields waiting to be parsed) |
|---|
| 155 | */ |
|---|
| 156 | protected $_pendingFields = array(); |
|---|
| 157 | |
|---|
| 158 | /** |
|---|
| 159 | * @var array $_parsers an array of parser objects, each DQL query part has its own parser |
|---|
| 160 | */ |
|---|
| 161 | protected $_parsers = array(); |
|---|
| 162 | |
|---|
| 163 | /** |
|---|
| 164 | * @var array $_pendingJoinConditions an array containing pending joins |
|---|
| 165 | */ |
|---|
| 166 | protected $_pendingJoinConditions = array(); |
|---|
| 167 | |
|---|
| 168 | /** |
|---|
| 169 | * @var array |
|---|
| 170 | */ |
|---|
| 171 | protected $_expressionMap = array(); |
|---|
| 172 | |
|---|
| 173 | /** |
|---|
| 174 | * @var string $_sql cached SQL query |
|---|
| 175 | */ |
|---|
| 176 | protected $_sql; |
|---|
| 177 | |
|---|
| 178 | |
|---|
| 179 | /** |
|---|
| 180 | * create |
|---|
| 181 | * returns a new Doctrine_Query object |
|---|
| 182 | * |
|---|
| 183 | * @param Doctrine_Connection $conn optional connection parameter |
|---|
| 184 | * @return Doctrine_Query |
|---|
| 185 | */ |
|---|
| 186 | public static function create($conn = null) |
|---|
| 187 | { |
|---|
| 188 | return new Doctrine_Query($conn); |
|---|
| 189 | } |
|---|
| 190 | |
|---|
| 191 | /** |
|---|
| 192 | * Resets the query to the state just after it has been instantiated. |
|---|
| 193 | */ |
|---|
| 194 | public function reset() |
|---|
| 195 | { |
|---|
| 196 | $this->_pendingJoinConditions = array(); |
|---|
| 197 | $this->_pendingSubqueries = array(); |
|---|
| 198 | $this->_pendingFields = array(); |
|---|
| 199 | $this->_neededTables = array(); |
|---|
| 200 | $this->_expressionMap = array(); |
|---|
| 201 | $this->_subqueryAliases = array(); |
|---|
| 202 | $this->_needsSubquery = false; |
|---|
| 203 | $this->_isLimitSubqueryUsed = false; |
|---|
| 204 | } |
|---|
| 205 | |
|---|
| 206 | /** |
|---|
| 207 | * createSubquery |
|---|
| 208 | * creates a subquery |
|---|
| 209 | * |
|---|
| 210 | * @return Doctrine_Hydrate |
|---|
| 211 | */ |
|---|
| 212 | public function createSubquery() |
|---|
| 213 | { |
|---|
| 214 | $class = get_class($this); |
|---|
| 215 | $obj = new $class(); |
|---|
| 216 | |
|---|
| 217 | // copy the aliases to the subquery |
|---|
| 218 | $obj->copyAliases($this); |
|---|
| 219 | |
|---|
| 220 | // this prevents the 'id' being selected, re ticket #307 |
|---|
| 221 | $obj->isSubquery(true); |
|---|
| 222 | |
|---|
| 223 | return $obj; |
|---|
| 224 | } |
|---|
| 225 | |
|---|
| 226 | /** |
|---|
| 227 | * _addPendingJoinCondition |
|---|
| 228 | * |
|---|
| 229 | * @param string $componentAlias component alias |
|---|
| 230 | * @param string $joinCondition dql join condition |
|---|
| 231 | * @return Doctrine_Query this object |
|---|
| 232 | */ |
|---|
| 233 | protected function _addPendingJoinCondition($componentAlias, $joinCondition) |
|---|
| 234 | { |
|---|
| 235 | $this->_pendingJoins[$componentAlias] = $joinCondition; |
|---|
| 236 | } |
|---|
| 237 | |
|---|
| 238 | /** |
|---|
| 239 | * addEnumParam |
|---|
| 240 | * sets input parameter as an enumerated parameter |
|---|
| 241 | * |
|---|
| 242 | * @param string $key the key of the input parameter |
|---|
| 243 | * @return Doctrine_Query |
|---|
| 244 | */ |
|---|
| 245 | public function addEnumParam($key, $table = null, $column = null) |
|---|
| 246 | { |
|---|
| 247 | $array = (isset($table) || isset($column)) ? array($table, $column) : array(); |
|---|
| 248 | |
|---|
| 249 | if ($key === '?') { |
|---|
| 250 | $this->_enumParams[] = $array; |
|---|
| 251 | } else { |
|---|
| 252 | $this->_enumParams[$key] = $array; |
|---|
| 253 | } |
|---|
| 254 | } |
|---|
| 255 | |
|---|
| 256 | /** |
|---|
| 257 | * getEnumParams |
|---|
| 258 | * get all enumerated parameters |
|---|
| 259 | * |
|---|
| 260 | * @return array all enumerated parameters |
|---|
| 261 | */ |
|---|
| 262 | public function getEnumParams() |
|---|
| 263 | { |
|---|
| 264 | return $this->_enumParams; |
|---|
| 265 | } |
|---|
| 266 | |
|---|
| 267 | /** |
|---|
| 268 | * getDql |
|---|
| 269 | * returns the DQL query that is represented by this query object. |
|---|
| 270 | * |
|---|
| 271 | * the query is built from $_dqlParts |
|---|
| 272 | * |
|---|
| 273 | * @return string the DQL query |
|---|
| 274 | */ |
|---|
| 275 | public function getDql() |
|---|
| 276 | { |
|---|
| 277 | $q = ''; |
|---|
| 278 | $q .= ( ! empty($this->_dqlParts['select']))? 'SELECT ' . implode(', ', $this->_dqlParts['select']) : ''; |
|---|
| 279 | $q .= ( ! empty($this->_dqlParts['from']))? ' FROM ' . implode(' ', $this->_dqlParts['from']) : ''; |
|---|
| 280 | $q .= ( ! empty($this->_dqlParts['where']))? ' WHERE ' . implode(' AND ', $this->_dqlParts['where']) : ''; |
|---|
| 281 | $q .= ( ! empty($this->_dqlParts['groupby']))? ' GROUP BY ' . implode(', ', $this->_dqlParts['groupby']) : ''; |
|---|
| 282 | $q .= ( ! empty($this->_dqlParts['having']))? ' HAVING ' . implode(' AND ', $this->_dqlParts['having']) : ''; |
|---|
| 283 | $q .= ( ! empty($this->_dqlParts['orderby']))? ' ORDER BY ' . implode(', ', $this->_dqlParts['orderby']) : ''; |
|---|
| 284 | $q .= ( ! empty($this->_dqlParts['limit']))? ' LIMIT ' . implode(' ', $this->_dqlParts['limit']) : ''; |
|---|
| 285 | $q .= ( ! empty($this->_dqlParts['offset']))? ' OFFSET ' . implode(' ', $this->_dqlParts['offset']) : ''; |
|---|
| 286 | |
|---|
| 287 | return $q; |
|---|
| 288 | } |
|---|
| 289 | |
|---|
| 290 | |
|---|
| 291 | /** |
|---|
| 292 | * fetchArray |
|---|
| 293 | * Convenience method to execute using array fetching as hydration mode. |
|---|
| 294 | * |
|---|
| 295 | * @param string $params |
|---|
| 296 | * @return array |
|---|
| 297 | */ |
|---|
| 298 | public function fetchArray($params = array()) { |
|---|
| 299 | return $this->execute($params, Doctrine::HYDRATE_ARRAY); |
|---|
| 300 | } |
|---|
| 301 | |
|---|
| 302 | /** |
|---|
| 303 | * fetchOne |
|---|
| 304 | * Convenience method to execute the query and return the first item |
|---|
| 305 | * of the collection. |
|---|
| 306 | * |
|---|
| 307 | * @param string $params Parameters |
|---|
| 308 | * @param int $hydrationMode Hydration mode |
|---|
| 309 | * @return mixed Array or Doctrine_Collection or false if no result. |
|---|
| 310 | */ |
|---|
| 311 | public function fetchOne($params = array(), $hydrationMode = null) |
|---|
| 312 | { |
|---|
| 313 | $collection = $this->execute($params, $hydrationMode); |
|---|
| 314 | |
|---|
| 315 | if (count($collection) === 0) { |
|---|
| 316 | return false; |
|---|
| 317 | } |
|---|
| 318 | |
|---|
| 319 | if ($collection instanceof Doctrine_Collection) { |
|---|
| 320 | return $collection->getFirst(); |
|---|
| 321 | } else if (is_array($collection)) { |
|---|
| 322 | return array_shift($collection); |
|---|
| 323 | } |
|---|
| 324 | |
|---|
| 325 | return false; |
|---|
| 326 | } |
|---|
| 327 | |
|---|
| 328 | /** |
|---|
| 329 | * isSubquery |
|---|
| 330 | * if $bool parameter is set this method sets the value of |
|---|
| 331 | * Doctrine_Query::$isSubquery. If this value is set to true |
|---|
| 332 | * the query object will not load the primary key fields of the selected |
|---|
| 333 | * components. |
|---|
| 334 | * |
|---|
| 335 | * If null is given as the first parameter this method retrieves the current |
|---|
| 336 | * value of Doctrine_Query::$isSubquery. |
|---|
| 337 | * |
|---|
| 338 | * @param boolean $bool whether or not this query acts as a subquery |
|---|
| 339 | * @return Doctrine_Query|bool |
|---|
| 340 | */ |
|---|
| 341 | public function isSubquery($bool = null) |
|---|
| 342 | { |
|---|
| 343 | if ($bool === null) { |
|---|
| 344 | return $this->_isSubquery; |
|---|
| 345 | } |
|---|
| 346 | |
|---|
| 347 | $this->_isSubquery = (bool) $bool; |
|---|
| 348 | return $this; |
|---|
| 349 | } |
|---|
| 350 | |
|---|
| 351 | /** |
|---|
| 352 | * getAggregateAlias |
|---|
| 353 | * |
|---|
| 354 | * @param string $dqlAlias the dql alias of an aggregate value |
|---|
| 355 | * @return string |
|---|
| 356 | * @deprecated |
|---|
| 357 | */ |
|---|
| 358 | public function getAggregateAlias($dqlAlias) |
|---|
| 359 | { |
|---|
| 360 | return $this->getSqlAggregateAlias($dqlAlias); |
|---|
| 361 | } |
|---|
| 362 | |
|---|
| 363 | /** |
|---|
| 364 | * getSqlAggregateAlias |
|---|
| 365 | * |
|---|
| 366 | * @param string $dqlAlias the dql alias of an aggregate value |
|---|
| 367 | * @return string |
|---|
| 368 | */ |
|---|
| 369 | public function getSqlAggregateAlias($dqlAlias) |
|---|
| 370 | { |
|---|
| 371 | if (isset($this->_aggregateAliasMap[$dqlAlias])) { |
|---|
| 372 | // mark the expression as used |
|---|
| 373 | $this->_expressionMap[$dqlAlias][1] = true; |
|---|
| 374 | |
|---|
| 375 | return $this->_aggregateAliasMap[$dqlAlias]; |
|---|
| 376 | } else if ( ! empty($this->_pendingAggregates)) { |
|---|
| 377 | $this->processPendingAggregates(); |
|---|
| 378 | |
|---|
| 379 | return $this->getSqlAggregateAlias($dqlAlias); |
|---|
| 380 | } else { |
|---|
| 381 | throw new Doctrine_Query_Exception('Unknown aggregate alias: ' . $dqlAlias); |
|---|
| 382 | } |
|---|
| 383 | } |
|---|
| 384 | |
|---|
| 385 | /** |
|---|
| 386 | * parseQueryPart |
|---|
| 387 | * parses given DQL query part |
|---|
| 388 | * |
|---|
| 389 | * @param string $queryPartName the name of the query part |
|---|
| 390 | * @param string $queryPart query part to be parsed |
|---|
| 391 | * @param boolean $append whether or not to append the query part to its stack |
|---|
| 392 | * if false is given, this method will overwrite |
|---|
| 393 | * the given query part stack with $queryPart |
|---|
| 394 | * @return Doctrine_Query this object |
|---|
| 395 | */ |
|---|
| 396 | /*protected function parseQueryPart($queryPartName, $queryPart, $append = false) |
|---|
| 397 | { |
|---|
| 398 | if ($this->_state === self::STATE_LOCKED) { |
|---|
| 399 | throw new Doctrine_Query_Exception('This query object is locked. No query parts can be manipulated.'); |
|---|
| 400 | } |
|---|
| 401 | |
|---|
| 402 | // sanity check |
|---|
| 403 | if ($queryPart === '' || $queryPart === null) { |
|---|
| 404 | throw new Doctrine_Query_Exception('Empty ' . $queryPartName . ' part given.'); |
|---|
| 405 | } |
|---|
| 406 | |
|---|
| 407 | // add query part to the dql part array |
|---|
| 408 | if ($append) { |
|---|
| 409 | $this->_dqlParts[$queryPartName][] = $queryPart; |
|---|
| 410 | } else { |
|---|
| 411 | $this->_dqlParts[$queryPartName] = array($queryPart); |
|---|
| 412 | } |
|---|
| 413 | |
|---|
| 414 | if ($this->_state === self::STATE_DIRECT) { |
|---|
| 415 | $parser = $this->_getParser($queryPartName); |
|---|
| 416 | |
|---|
| 417 | $sql = $parser->parse($queryPart); |
|---|
| 418 | |
|---|
| 419 | if (isset($sql)) { |
|---|
| 420 | if ($append) { |
|---|
| 421 | $this->addSqlQueryPart($queryPartName, $sql); |
|---|
| 422 | } else { |
|---|
| 423 | $this->setSqlQueryPart($queryPartName, $sql); |
|---|
| 424 | } |
|---|
| 425 | } |
|---|
| 426 | } |
|---|
| 427 | |
|---|
| 428 | $this->_state = Doctrine_Query::STATE_DIRTY; |
|---|
| 429 | |
|---|
| 430 | return $this; |
|---|
| 431 | }*/ |
|---|
| 432 | |
|---|
| 433 | /** |
|---|
| 434 | * getDqlPart |
|---|
| 435 | * returns a specific DQL query part. |
|---|
| 436 | * |
|---|
| 437 | * @param string $queryPart the name of the query part |
|---|
| 438 | * @return string the DQL query part |
|---|
| 439 | * @todo Description: List which query parts exist or point to the method/property |
|---|
| 440 | * where they are listed. |
|---|
| 441 | */ |
|---|
| 442 | public function getDqlPart($queryPart) |
|---|
| 443 | { |
|---|
| 444 | if ( ! isset($this->_dqlParts[$queryPart])) { |
|---|
| 445 | throw new Doctrine_Query_Exception('Unknown query part ' . $queryPart); |
|---|
| 446 | } |
|---|
| 447 | |
|---|
| 448 | return $this->_dqlParts[$queryPart]; |
|---|
| 449 | } |
|---|
| 450 | |
|---|
| 451 | /** |
|---|
| 452 | * contains |
|---|
| 453 | * |
|---|
| 454 | * Method to check if a arbitrary piece of dql exists |
|---|
| 455 | * |
|---|
| 456 | * @param string $dql Arbitrary piece of dql to check for |
|---|
| 457 | * @return boolean |
|---|
| 458 | */ |
|---|
| 459 | public function contains($dql) |
|---|
| 460 | { |
|---|
| 461 | return stripos($this->getDql(), $dql) === false ? false : true; |
|---|
| 462 | } |
|---|
| 463 | |
|---|
| 464 | /** |
|---|
| 465 | * processPendingFields |
|---|
| 466 | * the fields in SELECT clause cannot be parsed until the components |
|---|
| 467 | * in FROM clause are parsed, hence this method is called everytime a |
|---|
| 468 | * specific component is being parsed. |
|---|
| 469 | * |
|---|
| 470 | * @throws Doctrine_Query_Exception if unknown component alias has been given |
|---|
| 471 | * @param string $componentAlias the alias of the component |
|---|
| 472 | * @return void |
|---|
| 473 | * @todo Description: What is a 'pending field' (and are there non-pending fields, too)? |
|---|
| 474 | * What is 'processed'? (Meaning: What information is gathered & stored away) |
|---|
| 475 | */ |
|---|
| 476 | public function processPendingFields($componentAlias) |
|---|
| 477 | { |
|---|
| 478 | $tableAlias = $this->getTableAlias($componentAlias); |
|---|
| 479 | $table = $this->_queryComponents[$componentAlias]['table']; |
|---|
| 480 | |
|---|
| 481 | if ( ! isset($this->_pendingFields[$componentAlias])) { |
|---|
| 482 | if ($this->_hydrator->getHydrationMode() != Doctrine::HYDRATE_NONE) { |
|---|
| 483 | if ( ! $this->_isSubquery && $componentAlias == $this->getRootAlias()) { |
|---|
| 484 | throw new Doctrine_Query_Exception("The root class of the query (alias $componentAlias) " |
|---|
| 485 | . " must have at least one field selected."); |
|---|
| 486 | } |
|---|
| 487 | } |
|---|
| 488 | return; |
|---|
| 489 | } |
|---|
| 490 | |
|---|
| 491 | // At this point we know the component is FETCHED (either it's the base class of |
|---|
| 492 | // the query (FROM xyz) or its a "fetch join"). |
|---|
| 493 | |
|---|
| 494 | // Check that the parent join (if there is one), is a "fetch join", too. |
|---|
| 495 | if (isset($this->_queryComponents[$componentAlias]['parent'])) { |
|---|
| 496 | $parentAlias = $this->_queryComponents[$componentAlias]['parent']; |
|---|
| 497 | if (is_string($parentAlias) && ! isset($this->_pendingFields[$parentAlias]) |
|---|
| 498 | && $this->_hydrator->getHydrationMode() != Doctrine::HYDRATE_NONE) { |
|---|
| 499 | throw new Doctrine_Query_Exception("The left side of the join between " |
|---|
| 500 | . "the aliases '$parentAlias' and '$componentAlias' must have at least" |
|---|
| 501 | . " the primary key field(s) selected."); |
|---|
| 502 | } |
|---|
| 503 | } |
|---|
| 504 | |
|---|
| 505 | $fields = $this->_pendingFields[$componentAlias]; |
|---|
| 506 | |
|---|
| 507 | // check for wildcards |
|---|
| 508 | if (in_array('*', $fields)) { |
|---|
| 509 | $fields = $table->getFieldNames(); |
|---|
| 510 | } else { |
|---|
| 511 | // only auto-add the primary key fields if this query object is not |
|---|
| 512 | // a subquery of another query object |
|---|
| 513 | if ( ! $this->_isSubquery || $this->_hydrator->getHydrationMode() === Doctrine::HYDRATE_NONE) { |
|---|
| 514 | $fields = array_unique(array_merge((array) $table->getIdentifier(), $fields)); |
|---|
| 515 | } |
|---|
| 516 | } |
|---|
| 517 | |
|---|
| 518 | $sql = array(); |
|---|
| 519 | foreach ($fields as $fieldName) { |
|---|
| 520 | $columnName = $table->getColumnName($fieldName); |
|---|
| 521 | if (($owner = $table->getColumnOwner($columnName)) !== null && |
|---|
| 522 | $owner !== $table->getComponentName()) { |
|---|
| 523 | |
|---|
| 524 | $parent = $this->_conn->getTable($owner); |
|---|
| 525 | $columnName = $parent->getColumnName($fieldName); |
|---|
| 526 | $parentAlias = $this->getTableAlias($componentAlias . '.' . $parent->getComponentName()); |
|---|
| 527 | $sql[] = $this->_conn->quoteIdentifier($parentAlias . '.' . $columnName) |
|---|
| 528 | . ' AS ' |
|---|
| 529 | . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); |
|---|
| 530 | } else { |
|---|
| 531 | $columnName = $table->getColumnName($fieldName); |
|---|
| 532 | $sql[] = $this->_conn->quoteIdentifier($tableAlias . '.' . $columnName) |
|---|
| 533 | . ' AS ' |
|---|
| 534 | . $this->_conn->quoteIdentifier($tableAlias . '__' . $columnName); |
|---|
| 535 | } |
|---|
| 536 | } |
|---|
| 537 | |
|---|
| 538 | $this->_neededTables[] = $tableAlias; |
|---|
| 539 | |
|---|
| 540 | return implode(', ', $sql); |
|---|
| 541 | } |
|---|
| 542 | |
|---|
| 543 | /** |
|---|
| 544 | * parseSelectField |
|---|
| 545 | * |
|---|
| 546 | * @throws Doctrine_Query_Exception if unknown component alias has been given |
|---|
| 547 | * @return void |
|---|
| 548 | * @todo Description: Explain what this method does. Is there a relation to parseSelect()? |
|---|
| 549 | * (It doesnt seem to get called from there...?). In what circumstances is this method |
|---|
| 550 | * used? |
|---|
| 551 | */ |
|---|
| 552 | public function parseSelectField($field) |
|---|
| 553 | { |
|---|
| 554 | $terms = explode('.', $field); |
|---|
| 555 | |
|---|
| 556 | if (isset($terms[1])) { |
|---|
| 557 | $componentAlias = $terms[0]; |
|---|
| 558 | $field = $terms[1]; |
|---|
| 559 | } else { |
|---|
| 560 | reset($this->_queryComponents); |
|---|
| 561 | $componentAlias = key($this->_queryComponents); |
|---|
| 562 | $fields = $terms[0]; |
|---|
| 563 | } |
|---|
| 564 | |
|---|
| 565 | $tableAlias = $this->getTableAlias($componentAlias); |
|---|
| 566 | $table = $this->_queryComponents[$componentAlias]['table']; |
|---|
| 567 | |
|---|
| 568 | |
|---|
| 569 | // check for wildcards |
|---|
| 570 | if ($field === '*') { |
|---|
| 571 | $sql = array(); |
|---|
| 572 | |
|---|
| 573 | foreach ($table->getColumnNames() as $field) { |
|---|
| 574 | $sql[] = $this->parseSelectField($componentAlias . '.' . $field); |
|---|
| 575 | } |
|---|
| 576 | |
|---|
| 577 | return implode(', ', $sql); |
|---|
| 578 | } else { |
|---|
| 579 | $name = $table->getColumnName($field); |
|---|
| 580 | |
|---|
| 581 | $this->_neededTables[] = $tableAlias; |
|---|
| 582 | |
|---|
| 583 | return $this->_conn->quoteIdentifier($tableAlias . '.' . $name) |
|---|
| 584 | . ' AS ' |
|---|
| 585 | . $this->_conn->quoteIdentifier($tableAlias . '__' . $name); |
|---|
| 586 | } |
|---|
| 587 | } |
|---|
| 588 | |
|---|
| 589 | /** |
|---|
| 590 | * getExpressionOwner |
|---|
| 591 | * returns the component alias for owner of given expression |
|---|
| 592 | * |
|---|
| 593 | * @param string $expr expression from which to get to owner from |
|---|
| 594 | * @return string the component alias |
|---|
| 595 | * @todo Description: What does it mean if a component is an 'owner' of an expression? |
|---|
| 596 | * What kind of 'expression' are we talking about here? |
|---|
| 597 | */ |
|---|
| 598 | public function getExpressionOwner($expr) |
|---|
| 599 | { |
|---|
| 600 | if (strtoupper(substr(trim($expr, '( '), 0, 6)) !== 'SELECT') { |
|---|
| 601 | preg_match_all("/[a-z_][a-z0-9_]*\.[a-z_][a-z0-9_]*[\.[a-z0-9]+]*/i", $expr, $matches); |
|---|
| 602 | |
|---|
| 603 | $match = current($matches); |
|---|
| 604 | |
|---|
| 605 | if (isset($match[0])) { |
|---|
| 606 | $terms = explode('.', $match[0]); |
|---|
| 607 | |
|---|
| 608 | return $terms[0]; |
|---|
| 609 | } |
|---|
| 610 | } |
|---|
| 611 | return $this->getRootAlias(); |
|---|
| 612 | |
|---|
| 613 | } |
|---|
| 614 | |
|---|
| 615 | /** |
|---|
| 616 | * parseSelect |
|---|
| 617 | * parses the query select part and |
|---|
| 618 | * adds selected fields to pendingFields array |
|---|
| 619 | * |
|---|
| 620 | * @param string $dql |
|---|
| 621 | * @todo Description: What information is extracted (and then stored)? |
|---|
| 622 | */ |
|---|
| 623 | public function parseSelect($dql) |
|---|
| 624 | { |
|---|
| 625 | $refs = $this->_tokenizer->sqlExplode($dql, ','); |
|---|
| 626 | |
|---|
| 627 | $pos = strpos(trim($refs[0]), ' '); |
|---|
| 628 | $first = substr($refs[0], 0, $pos); |
|---|
| 629 | |
|---|
| 630 | // check for DISTINCT keyword |
|---|
| 631 | if ($first === 'DISTINCT') { |
|---|
| 632 | $this->_sqlParts['distinct'] = true; |
|---|
| 633 | |
|---|
| 634 | $refs[0] = substr($refs[0], ++$pos); |
|---|
| 635 | } |
|---|
| 636 | |
|---|
| 637 | $parsedComponents = array(); |
|---|
| 638 | |
|---|
| 639 | foreach ($refs as $reference) { |
|---|
| 640 | $reference = trim($reference); |
|---|
| 641 | |
|---|
| 642 | if (empty($reference)) { |
|---|
| 643 | continue; |
|---|
| 644 | } |
|---|
| 645 | |
|---|
| 646 | $terms = $this->_tokenizer->sqlExplode($reference, ' '); |
|---|
| 647 | |
|---|
| 648 | $pos = strpos($terms[0], '('); |
|---|
| 649 | |
|---|
| 650 | if (count($terms) > 1 || $pos !== false) { |
|---|
| 651 | $expression = array_shift($terms); |
|---|
| 652 | $alias = array_pop($terms); |
|---|
| 653 | |
|---|
| 654 | if ( ! $alias) { |
|---|
| 655 | $alias = substr($expression, 0, $pos); |
|---|
| 656 | } |
|---|
| 657 | |
|---|
| 658 | $componentAlias = $this->getExpressionOwner($expression); |
|---|
| 659 | $expression = $this->parseClause($expression); |
|---|
| 660 | |
|---|
| 661 | $tableAlias = $this->getTableAlias($componentAlias); |
|---|
| 662 | |
|---|
| 663 | $index = count($this->_aggregateAliasMap); |
|---|
| 664 | |
|---|
| 665 | $sqlAlias = $this->_conn->quoteIdentifier($tableAlias . '__' . $index); |
|---|
| 666 | |
|---|
| 667 | $this->_sqlParts['select'][] = $expression . ' AS ' . $sqlAlias; |
|---|
| 668 | |
|---|
| 669 | $this->_aggregateAliasMap[$alias] = $sqlAlias; |
|---|
| 670 | $this->_expressionMap[$alias][0] = $expression; |
|---|
| 671 | |
|---|
| 672 | $this->_queryComponents[$componentAlias]['agg'][$index] = $alias; |
|---|
| 673 | |
|---|
| 674 | $this->_neededTables[] = $tableAlias; |
|---|
| 675 | } else { |
|---|
| 676 | $e = explode('.', $terms[0]); |
|---|
| 677 | |
|---|
| 678 | if (isset($e[1])) { |
|---|
| 679 | $componentAlias = $e[0]; |
|---|
| 680 | $field = $e[1]; |
|---|
| 681 | } else { |
|---|
| 682 | reset($this->_queryComponents); |
|---|
| 683 | $componentAlias = key($this->_queryComponents); |
|---|
| 684 | $field = $e[0]; |
|---|
| 685 | } |
|---|
| 686 | |
|---|
| 687 | $this->_pendingFields[$componentAlias][] = $field; |
|---|
| 688 | } |
|---|
| 689 | } |
|---|
| 690 | } |
|---|
| 691 | |
|---|
| 692 | /** |
|---|
| 693 | * parseClause |
|---|
| 694 | * parses given DQL clause |
|---|
| 695 | * |
|---|
| 696 | * this method handles five tasks: |
|---|
| 697 | * |
|---|
| 698 | * 1. Converts all DQL functions to their native SQL equivalents |
|---|
| 699 | * 2. Converts all component references to their table alias equivalents |
|---|
| 700 | * 3. Converts all field names to actual column names |
|---|
| 701 | * 4. Quotes all identifiers |
|---|
| 702 | * 5. Parses nested clauses and subqueries recursively |
|---|
| 703 | * |
|---|
| 704 | * @return string SQL string |
|---|
| 705 | * @todo Description: What is a 'dql clause' (and what not)? |
|---|
| 706 | * Refactor: Too long & nesting level |
|---|
| 707 | */ |
|---|
| 708 | public function parseClause($clause) |
|---|
| 709 | { |
|---|
| 710 | $clause = trim($clause); |
|---|
| 711 | |
|---|
| 712 | if (is_numeric($clause)) { |
|---|
| 713 | return $clause; |
|---|
| 714 | } |
|---|
| 715 | |
|---|
| 716 | $terms = $this->_tokenizer->clauseExplode($clause, array(' ', '+', '-', '*', '/', '<', '>', '=', '>=', '<=')); |
|---|
| 717 | |
|---|
| 718 | $str = ''; |
|---|
| 719 | foreach ($terms as $term) { |
|---|
| 720 | $pos = strpos($term[0], '('); |
|---|
| 721 | |
|---|
| 722 | if ($pos !== false) { |
|---|
| 723 | $name = substr($term[0], 0, $pos); |
|---|
| 724 | $term[0] = $this->parseFunctionExpression($term[0]); |
|---|
| 725 | } else { |
|---|
| 726 | if (substr($term[0], 0, 1) !== "'" && substr($term[0], -1) !== "'") |
|---|