Query.php 14 KB
Newer Older
1 2
<?php
/*
3
 *  $Id$
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
 *
 * 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
19
 * <http://www.doctrine-project.org>.
20
 */
21

22 23
namespace Doctrine\ORM;

24 25
use Doctrine\ORM\Query\Parser,
    Doctrine\ORM\Query\QueryException;
26

27
/**
28
 * A Query object represents a DQL query.
29 30
 *
 * @license     http://www.opensource.org/licenses/lgpl-license.php LGPL
romanb's avatar
romanb committed
31
 * @link        www.doctrine-project.org
32
 * @since       1.0
33 34
 * @version     $Revision: 3938 $
 * @author      Guilherme Blanco <guilhermeblanco@hotmail.com>
35
 * @author      Konsta Vesterinen <kvesteri@cc.hut.fi>
36
 * @author      Roman Borschel <roman@code-factory.org>
37
 */
38
final class Query extends AbstractQuery
zYne's avatar
zYne committed
39
{
40
    /* Query STATES */
41
    /**
42
     * A query object is in CLEAN state when it has NO unparsed/unprocessed DQL parts.
43
     */
44
    const STATE_CLEAN  = 1;
45
    /**
46 47 48
     * A query object is in state DIRTY when it has DQL parts that have not yet been
     * parsed/processed. This is automatically defined as DIRTY when addDqlQueryPart
     * is called.
49
     */
romanb's avatar
romanb committed
50
    const STATE_DIRTY = 2;
51 52 53 54 55 56 57 58 59 60 61
    
    /* Query HINTS */
    /**
     * The refresh hint turns any query into a refresh query with the result that
     * any local changes in entities are overridden with the fetched values.
     * 
     * @var string
     */
    const HINT_REFRESH = 'doctrine.refresh';
    /**
     * The forcePartialLoad query hint forces a particular query to return
62
     * partial objects.
63 64 65 66 67 68 69 70 71 72 73 74 75
     * 
     * @var string
     */
    const HINT_FORCE_PARTIAL_LOAD = 'doctrine.forcePartialLoad';
    /**
     * The includeMetaColumns query hint causes meta columns like foreign keys and
     * discriminator columns to be selected and returned as part of the query result.
     * 
     * This hint does only apply to non-object queries.
     * 
     * @var string
     */
    const HINT_INCLUDE_META_COLUMNS = 'doctrine.includeMetaColumns';
76
    
77
    const HINT_CUSTOM_TREE_WALKERS = 'doctrine.customTreeWalkers';
78
    //const HINT_READ_ONLY = 'doctrine.readOnly';
79

80 81 82 83 84
    /**
     * @var string
     */
    const HINT_INTERNAL_ITERATION = 'doctrine.internal.iteration';

zYne's avatar
zYne committed
85
    /**
86
     * @var integer $_state   The current state of this query.
zYne's avatar
zYne committed
87
     */
88
    private $_state = self::STATE_CLEAN;
zYne's avatar
zYne committed
89

90
    /**
91
     * @var string $_dql Cached DQL query.
92
     */
93
    private $_dql = null;
94

95 96 97
    /**
     * @var Doctrine\ORM\Query\ParserResult  The parser result that holds DQL => SQL information.
     */
98
    private $_parserResult;
romanb's avatar
romanb committed
99 100 101 102 103 104 105 106 107 108
    
    /**
     * @var integer The first result to return (the "offset").
     */
    private $_firstResult = null;
    
    /**
     * @var integer The maximum number of results to return (the "limit").
     */
    private $_maxResults = null;
109 110

    /**
111
     * @var CacheDriver  The cache driver used for caching queries.
112
     */
113
    private $_queryCache;
114

115
    /**
116
     * @var boolean Boolean value that indicates whether or not expire the query cache.
117
     */
118
    private $_expireQueryCache = false;
119

120
    /**
121
     * @var int Query Cache lifetime.
122
     */
123
    private $_queryCacheTTL;
124 125 126 127 128
    
    /**
     * @var boolean Whether to use a query cache, if available. Defaults to TRUE.
     */
    private $_useQueryCache = true;
129 130 131

    // End of Caching Stuff

132
    /**
133
     * Initializes a new Query instance.
134
     *
romanb's avatar
romanb committed
135
     * @param Doctrine\ORM\EntityManager $entityManager
136
     */
137
    public function __construct(EntityManager $entityManager)
138
    {
139
        parent::__construct($entityManager);
140
    }
141

142
    /**
143
     * Gets the SQL query/queries that correspond to this DQL query.
144
     *
145 146
     * @return mixed The built sql query or an array of all sql queries.
     * @override
147
     */
148
    public function getSql()
149
    {
romanb's avatar
romanb committed
150
        return $this->_parse()->getSqlExecutor()->getSqlStatements();
151
    }
152

153
    /**
154 155 156
     * Parses the DQL query, if necessary, and stores the parser result.
     * 
     * Note: Populates $this->_parserResult as a side-effect.
157
     *
158
     * @return Doctrine\ORM\Query\ParserResult
159
     */
romanb's avatar
romanb committed
160
    private function _parse()
161
    {
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179
        if ($this->_state === self::STATE_CLEAN) {
            return $this->_parserResult;
        }
        
        // Check query cache.
        if ($this->_useQueryCache && ($queryCache = $this->getQueryCacheDriver())) {
            $hash = $this->_getQueryCacheId();
            $cached = $this->_expireQueryCache ? false : $queryCache->fetch($hash);
            if ($cached === false) {
                // Cache miss.
                $parser = new Parser($this);
                $this->_parserResult = $parser->parse();
                $queryCache->save($hash, $this->_parserResult, null);
            } else {
                // Cache hit.
                $this->_parserResult = $cached;
            }
        } else {
180 181 182
            $parser = new Parser($this);
            $this->_parserResult = $parser->parse();
        }
183 184
        $this->_state = self::STATE_CLEAN;
        
185
        return $this->_parserResult;
186
    }
187

188
    /**
romanb's avatar
romanb committed
189
     * {@inheritdoc}
190
     *
191
     * @param array $params
romanb's avatar
romanb committed
192
     * @return Statement The resulting Statement.
193
     * @override
194
     */
195
    protected function _doExecute(array $params)
196
    {
197
        $executor = $this->_parse()->getSqlExecutor();
198 199 200
        $params = $this->_prepareParams($params);
        if ( ! $this->_resultSetMapping) {
            $this->_resultSetMapping = $this->_parserResult->getResultSetMapping();
201 202
        }

203
        return $executor->execute($this->_em->getConnection(), $params);
zYne's avatar
zYne committed
204
    }
romanb's avatar
romanb committed
205 206 207 208 209 210 211 212 213 214 215
    
    /**
     * {@inheritdoc}
     *
     * @override
     */
    protected function _prepareParams(array $params)
    {
        $sqlParams = array();
        
        $paramMappings = $this->_parserResult->getParameterMappings();
216

217
        if (count($paramMappings) != count($params)) {
218
            throw QueryException::invalidParameterNumber();
219 220
        }

romanb's avatar
romanb committed
221
        foreach ($params as $key => $value) {
222
            if ( ! isset($paramMappings[$key])) {
223
                throw QueryException::unknownParameter($key);
224 225
            }

romanb's avatar
romanb committed
226
            if (is_object($value)) {
romanb's avatar
romanb committed
227 228
                //$values = $this->_em->getClassMetadata(get_class($value))->getIdentifierValues($value);
                $values = $this->_em->getUnitOfWork()->getEntityIdentifier($value);
romanb's avatar
romanb committed
229
                //var_dump($this->_em->getUnitOfWork()->getEntityIdentifier($value));
romanb's avatar
romanb committed
230
                $sqlPositions = $paramMappings[$key];
romanb's avatar
romanb committed
231
                $sqlParams = array_merge($sqlParams, array_combine((array)$sqlPositions, $values));
romanb's avatar
romanb committed
232 233 234 235 236 237 238 239 240 241 242 243 244 245 246
            } else if (is_bool($value)) {
                $boolValue = $this->_em->getConnection()->getDatabasePlatform()->convertBooleans($value);
                foreach ($paramMappings[$key] as $position) {
                    $sqlParams[$position] = $boolValue;
                }
            } else {
                foreach ($paramMappings[$key] as $position) {
                    $sqlParams[$position] = $value;
                }
            }
        }
        ksort($sqlParams);
        
        return array_values($sqlParams);
    }
247

248
    /**
249
     * Defines a cache driver to be used for caching queries.
250
     *
251
     * @param Doctrine_Cache_Interface|null $driver Cache driver
252
     * @return Query This query instance.
253
     */
254
    public function setQueryCacheDriver($queryCache)
255
    {
256 257
        $this->_queryCache = $queryCache;
        return $this;
258
    }
259 260 261 262 263 264 265 266 267 268 269 270
    
    /**
     * Defines whether the query should make use of a query cache, if available.
     * 
     * @param boolean $bool
     * @return @return Query This query instance.
     */
    public function useQueryCache($bool)
    {
        $this->_useQueryCache = $bool;
        return $this;
    }
271

zYne's avatar
zYne committed
272
    /**
273
     * Returns the cache driver used for query caching.
zYne's avatar
zYne committed
274
     *
275 276
     * @return CacheDriver The cache driver used for query caching or NULL, if this
     * 					   Query does not use query caching.
zYne's avatar
zYne committed
277
     */
278
    public function getQueryCacheDriver()
zYne's avatar
zYne committed
279
    {
280
        if ($this->_queryCache) {
281 282
            return $this->_queryCache;
        } else {
283
            return $this->_em->getConfiguration()->getQueryCacheImpl();
284
        }
285
    }
286

zYne's avatar
zYne committed
287
    /**
288
     * Defines how long the query cache will be active before expire.
zYne's avatar
zYne committed
289
     *
290
     * @param integer $timeToLive How long the cache entry is valid
291
     * @return Query This query instance.
zYne's avatar
zYne committed
292
     */
293
    public function setQueryCacheLifetime($timeToLive)
zYne's avatar
zYne committed
294
    {
295 296
        if ($timeToLive !== null) {
            $timeToLive = (int) $timeToLive;
romanb's avatar
romanb committed
297
        }
298 299 300
        $this->_queryCacheTTL = $timeToLive;

        return $this;
301
    }
302 303 304 305 306 307

    /**
     * Retrieves the lifetime of resultset cache.
     *
     * @return int
     */
308
    public function getQueryCacheLifetime()
309 310
    {
        return $this->_queryCacheTTL;
311
    }
312 313 314 315 316

    /**
     * Defines if the query cache is active or not.
     *
     * @param boolean $expire Whether or not to force query cache expiration.
317
     * @return Query This query instance.
318
     */
319
    public function expireQueryCache($expire = true)
320
    {
321
        $this->_expireQueryCache = $expire;
322 323

        return $this;
324
    }
325 326 327 328 329 330

    /**
     * Retrieves if the query cache is active or not.
     *
     * @return bool
     */
331
    public function getExpireQueryCache()
332 333
    {
        return $this->_expireQueryCache;
334
    }
335 336 337 338 339 340 341 342 343

    /**
     * @override
     */
    public function free()
    {
        parent::free();
        $this->_dql = null;
        $this->_state = self::STATE_CLEAN;
zYne's avatar
zYne committed
344
    }
345

zYne's avatar
zYne committed
346
    /**
347
     * Sets a DQL query string.
zYne's avatar
zYne committed
348
     *
349
     * @param string $dqlQuery DQL Query
350
     * @return Doctrine\ORM\AbstractQuery
zYne's avatar
zYne committed
351
     */
352
    public function setDql($dqlQuery)
zYne's avatar
zYne committed
353
    {
354 355 356
        if ($dqlQuery !== null) {
            $this->_dql = $dqlQuery;
            $this->_state = self::STATE_DIRTY;
357
        }
358
        return $this;
359
    }
360

361 362 363 364 365 366 367
    /**
     * Returns the DQL query that is represented by this query object.
     *
     * @return string DQL query
     */
    public function getDql()
    {
368
        return $this->_dql;
369
    }
zYne's avatar
zYne committed
370

371 372 373 374 375 376 377 378 379 380 381 382 383
    /**
     * Returns the state of this query object
     * By default the type is Doctrine_ORM_Query_Abstract::STATE_CLEAN but if it appears any unprocessed DQL
     * part, it is switched to Doctrine_ORM_Query_Abstract::STATE_DIRTY.
     *
     * @see AbstractQuery::STATE_CLEAN
     * @see AbstractQuery::STATE_DIRTY
     *
     * @return integer Return the query state
     */
    public function getState()
    {
        return $this->_state;
zYne's avatar
zYne committed
384
    }
385

386
    /**
387
     * Method to check if an arbitrary piece of DQL exists
388
     *
389 390
     * @param string $dql Arbitrary piece of DQL to check for
     * @return boolean
391
     */
392
    public function contains($dql)
393
    {
romanb's avatar
romanb committed
394 395 396 397 398 399 400 401 402 403 404 405
        return stripos($this->getDql(), $dql) === false ? false : true;
    }
    
    /**
     * Sets the position of the first result to retrieve (the "offset").
     *
     * @param integer $firstResult The first result to return.
     * @return Query This query object.
     */
    public function setFirstResult($firstResult)
    {
        $this->_firstResult = $firstResult;
406
        $this->_state = self::STATE_DIRTY;
romanb's avatar
romanb committed
407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429
        return $this;
    }
    
    /**
     * Gets the position of the first result the query object was set to retrieve (the "offset").
     * Returns NULL if {@link setFirstResult} was not applied to this query.
     * 
     * @return integer The position of the first result.
     */
    public function getFirstResult()
    {
        return $this->_firstResult;
    }
    
    /**
     * Sets the maximum number of results to retrieve (the "limit").
     * 
     * @param integer $maxResults
     * @return Query This query object.
     */
    public function setMaxResults($maxResults)
    {
        $this->_maxResults = $maxResults;
430
        $this->_state = self::STATE_DIRTY;
romanb's avatar
romanb committed
431 432 433 434 435 436 437 438 439 440 441 442
        return $this;
    }
    
    /**
     * Gets the maximum number of results the query object was set to retrieve (the "limit").
     * Returns NULL if {@link setMaxResults} was not applied to this query.
     * 
     * @return integer Maximum number of results.
     */
    public function getMaxResults()
    {
        return $this->_maxResults;
443
    }
444 445 446 447 448 449 450 451 452 453 454 455 456 457

    /**
     * Executes the query and returns an IterableResult that can be used to incrementally
     * iterated over the result.
     *
     * @param array $params The query parameters.
     * @param integer $hydrationMode The hydration mode to use.
     * @return IterableResult
     */
    public function iterate(array $params = array(), $hydrationMode = self::HYDRATE_OBJECT)
    {
        $this->setHint(self::HINT_INTERNAL_ITERATION, true);
        return parent::iterate($params, $hydrationMode);
    }
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475
    
    /**
     * {@inheritdoc}
     */
    public function setHint($name, $value)
    {
        $this->_state = self::STATE_DIRTY;
        return parent::setHint($name, $value);
    }
    
    /**
     * {@inheritdoc}
     */
    public function setHydrationMode($hydrationMode)
    {
        $this->_state = self::STATE_DIRTY;
        return parent::setHydrationMode($hydrationMode);
    }
476 477 478 479 480 481 482 483 484 485 486 487 488 489

    /**
     * Generate a cache id for the query cache - reusing the Result-Cache-Id generator.
     *
     * The query cache
     *
     * @return string
     */
    protected function _getQueryCacheId()
    {
        ksort($this->_hints);

        return md5(
            $this->getDql() . var_export($this->_hints, true) . 
490 491
            '&firstResult=' . $this->_firstResult . '&maxResult=' . $this->_maxResults .
            '&hydrationMode='.$this->_hydrationMode.'DOCTRINE_QUERY_CACHE_SALT'
492 493
        );
    }
494
}