1<?php
2namespace TYPO3\CMS\Extbase\Persistence\Generic;
3
4/*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17use TYPO3\CMS\Extbase\Persistence\QueryInterface;
18
19/**
20 * The Query class used to run queries against the database
21 */
22class Query implements QueryInterface
23{
24    /**
25     * An inner join.
26     */
27    const JCR_JOIN_TYPE_INNER = '{http://www.jcp.org/jcr/1.0}joinTypeInner';
28
29    /**
30     * A left-outer join.
31     */
32    const JCR_JOIN_TYPE_LEFT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeLeftOuter';
33
34    /**
35     * A right-outer join.
36     */
37    const JCR_JOIN_TYPE_RIGHT_OUTER = '{http://www.jcp.org/jcr/1.0}joinTypeRightOuter';
38
39    /**
40     * Charset of strings in QOM
41     */
42    const CHARSET = 'utf-8';
43
44    /**
45     * @var string
46     */
47    protected $type;
48
49    /**
50     * @var \TYPO3\CMS\Extbase\Object\ObjectManagerInterface
51     */
52    protected $objectManager;
53
54    /**
55     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory
56     */
57    protected $dataMapFactory;
58
59    /**
60     * @var \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface
61     */
62    protected $persistenceManager;
63
64    /**
65     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory
66     */
67    protected $qomFactory;
68
69    /**
70     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface
71     */
72    protected $source;
73
74    /**
75     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface
76     */
77    protected $constraint;
78
79    /**
80     * @var \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement
81     */
82    protected $statement;
83
84    /**
85     * @var int
86     */
87    protected $orderings = [];
88
89    /**
90     * @var int
91     */
92    protected $limit;
93
94    /**
95     * @var int
96     */
97    protected $offset;
98
99    /**
100     * The query settings.
101     *
102     * @var QuerySettingsInterface
103     */
104    protected $querySettings;
105
106    /**
107     * @var ?QueryInterface
108     * @internal
109     */
110    protected $parentQuery;
111
112    /**
113     * @param \TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager
114     */
115    public function injectObjectManager(\TYPO3\CMS\Extbase\Object\ObjectManagerInterface $objectManager)
116    {
117        $this->objectManager = $objectManager;
118    }
119
120    /**
121     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory
122     */
123    public function injectDataMapFactory(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory $dataMapFactory)
124    {
125        $this->dataMapFactory = $dataMapFactory;
126    }
127
128    /**
129     * @param \TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager
130     */
131    public function injectPersistenceManager(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface $persistenceManager)
132    {
133        $this->persistenceManager = $persistenceManager;
134    }
135
136    /**
137     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory
138     */
139    public function injectQomFactory(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory $qomFactory)
140    {
141        $this->qomFactory = $qomFactory;
142    }
143
144    /**
145     * Constructs a query object working on the given class name
146     *
147     * @param string $type
148     */
149    public function __construct($type)
150    {
151        $this->type = $type;
152    }
153
154    /**
155     * @return ?QueryInterface
156     * @internal
157     */
158    public function getParentQuery(): ?QueryInterface
159    {
160        return $this->parentQuery;
161    }
162
163    /**
164     * @param ?QueryInterface $parentQuery
165     * @internal
166     */
167    public function setParentQuery(?QueryInterface $parentQuery): void
168    {
169        $this->parentQuery = $parentQuery;
170    }
171
172    /**
173     * Sets the Query Settings. These Query settings must match the settings expected by
174     * the specific Storage Backend.
175     *
176     * @param QuerySettingsInterface $querySettings The Query Settings
177     */
178    public function setQuerySettings(QuerySettingsInterface $querySettings)
179    {
180        $this->querySettings = $querySettings;
181    }
182
183    /**
184     * Returns the Query Settings.
185     *
186     * @throws Exception
187     * @return QuerySettingsInterface $querySettings The Query Settings
188     */
189    public function getQuerySettings()
190    {
191        if (!$this->querySettings instanceof QuerySettingsInterface) {
192            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception('Tried to get the query settings without seting them before.', 1248689115);
193        }
194        return $this->querySettings;
195    }
196
197    /**
198     * Returns the type this query cares for.
199     *
200     * @return string
201     */
202    public function getType()
203    {
204        return $this->type;
205    }
206
207    /**
208     * Sets the source to fetch the result from
209     *
210     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source
211     */
212    public function setSource(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface $source)
213    {
214        $this->source = $source;
215    }
216
217    /**
218     * Returns the selectorn name or an empty string, if the source is not a selector
219     * @todo This has to be checked at another place
220     *
221     * @return string The selector name
222     */
223    protected function getSelectorName()
224    {
225        $source = $this->getSource();
226        if ($source instanceof \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SelectorInterface) {
227            return $source->getSelectorName();
228        }
229        return '';
230    }
231
232    /**
233     * Gets the node-tuple source for this query.
234     *
235     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\SourceInterface the node-tuple source; non-null
236     */
237    public function getSource()
238    {
239        if ($this->source === null) {
240            $this->source = $this->qomFactory->selector($this->getType(), $this->dataMapFactory->buildDataMap($this->getType())->getTableName());
241        }
242        return $this->source;
243    }
244
245    /**
246     * Executes the query against the database and returns the result
247     *
248     * @param bool $returnRawQueryResult avoids the object mapping by the persistence
249     * @return \TYPO3\CMS\Extbase\Persistence\QueryResultInterface|array The query result object or an array if $returnRawQueryResult is TRUE
250     */
251    public function execute($returnRawQueryResult = false)
252    {
253        if ($returnRawQueryResult) {
254            return $this->persistenceManager->getObjectDataByQuery($this);
255        }
256        return $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\QueryResultInterface::class, $this);
257    }
258
259    /**
260     * Sets the property names to order the result by. Expected like this:
261     * array(
262     * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
263     * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
264     * )
265     * where 'foo' and 'bar' are property names.
266     *
267     * @param array $orderings The property names to order by
268     * @return QueryInterface
269     */
270    public function setOrderings(array $orderings)
271    {
272        $this->orderings = $orderings;
273        return $this;
274    }
275
276    /**
277     * Returns the property names to order the result by. Like this:
278     * array(
279     * 'foo' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_ASCENDING,
280     * 'bar' => \TYPO3\CMS\Extbase\Persistence\QueryInterface::ORDER_DESCENDING
281     * )
282     *
283     * @return int
284     */
285    public function getOrderings()
286    {
287        return $this->orderings;
288    }
289
290    /**
291     * Sets the maximum size of the result set to limit. Returns $this to allow
292     * for chaining (fluid interface)
293     *
294     * @param int $limit
295     * @throws \InvalidArgumentException
296     * @return QueryInterface
297     */
298    public function setLimit($limit)
299    {
300        if (!is_int($limit) || $limit < 1) {
301            throw new \InvalidArgumentException('The limit must be an integer >= 1', 1245071870);
302        }
303        $this->limit = $limit;
304        return $this;
305    }
306
307    /**
308     * Resets a previously set maximum size of the result set. Returns $this to allow
309     * for chaining (fluid interface)
310     *
311     * @return QueryInterface
312     */
313    public function unsetLimit()
314    {
315        unset($this->limit);
316        return $this;
317    }
318
319    /**
320     * Returns the maximum size of the result set to limit.
321     *
322     * @return int
323     */
324    public function getLimit()
325    {
326        return $this->limit;
327    }
328
329    /**
330     * Sets the start offset of the result set to offset. Returns $this to
331     * allow for chaining (fluid interface)
332     *
333     * @param int $offset
334     * @throws \InvalidArgumentException
335     * @return QueryInterface
336     */
337    public function setOffset($offset)
338    {
339        if (!is_int($offset) || $offset < 0) {
340            throw new \InvalidArgumentException('The offset must be a positive integer', 1245071872);
341        }
342        $this->offset = $offset;
343        return $this;
344    }
345
346    /**
347     * Returns the start offset of the result set.
348     *
349     * @return int
350     */
351    public function getOffset()
352    {
353        return $this->offset;
354    }
355
356    /**
357     * The constraint used to limit the result set. Returns $this to allow
358     * for chaining (fluid interface)
359     *
360     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint
361     * @return QueryInterface
362     */
363    public function matching($constraint)
364    {
365        $this->constraint = $constraint;
366        return $this;
367    }
368
369    /**
370     * Sets the statement of this query. If you use this, you will lose the abstraction from a concrete storage
371     * backend (database).
372     *
373     * @param string|\TYPO3\CMS\Core\Database\Query\QueryBuilder|\Doctrine\DBAL\Statement $statement The statement
374     * @param array $parameters An array of parameters. These will be bound to placeholders '?' in the $statement.
375     * @return QueryInterface
376     */
377    public function statement($statement, array $parameters = [])
378    {
379        $this->statement = $this->qomFactory->statement($statement, $parameters);
380        return $this;
381    }
382
383    /**
384     * Returns the statement of this query.
385     *
386     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\Statement
387     */
388    public function getStatement()
389    {
390        return $this->statement;
391    }
392
393    /**
394     * Gets the constraint for this query.
395     *
396     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface|null the constraint, or null if none
397     */
398    public function getConstraint()
399    {
400        return $this->constraint;
401    }
402
403    /**
404     * Performs a logical conjunction of the given constraints. The method takes one or more constraints and concatenates them with a boolean AND.
405     * It also accepts a single array of constraints to be concatenated.
406     *
407     * @param mixed $constraint1 The first of multiple constraints or an array of constraints.
408     * @throws Exception\InvalidNumberOfConstraintsException
409     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface
410     */
411    public function logicalAnd($constraint1)
412    {
413        if (is_array($constraint1)) {
414            $resultingConstraint = array_shift($constraint1);
415            $constraints = $constraint1;
416        } else {
417            $constraints = func_get_args();
418            $resultingConstraint = array_shift($constraints);
419        }
420        if ($resultingConstraint === null) {
421            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1268056288);
422        }
423        foreach ($constraints as $constraint) {
424            $resultingConstraint = $this->qomFactory->_and($resultingConstraint, $constraint);
425        }
426        return $resultingConstraint;
427    }
428
429    /**
430     * Performs a logical disjunction of the two given constraints
431     *
432     * @param mixed $constraint1 The first of multiple constraints or an array of constraints.
433     * @throws Exception\InvalidNumberOfConstraintsException
434     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\OrInterface
435     */
436    public function logicalOr($constraint1)
437    {
438        if (is_array($constraint1)) {
439            $resultingConstraint = array_shift($constraint1);
440            $constraints = $constraint1;
441        } else {
442            $constraints = func_get_args();
443            $resultingConstraint = array_shift($constraints);
444        }
445        if ($resultingConstraint === null) {
446            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidNumberOfConstraintsException('There must be at least one constraint or a non-empty array of constraints given.', 1268056289);
447        }
448        foreach ($constraints as $constraint) {
449            $resultingConstraint = $this->qomFactory->_or($resultingConstraint, $constraint);
450        }
451        return $resultingConstraint;
452    }
453
454    /**
455     * Performs a logical negation of the given constraint
456     *
457     * @param \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint Constraint to negate
458     * @throws \RuntimeException
459     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\NotInterface
460     */
461    public function logicalNot(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\ConstraintInterface $constraint)
462    {
463        return $this->qomFactory->not($constraint);
464    }
465
466    /**
467     * Returns an equals criterion used for matching objects against a query
468     *
469     * @param string $propertyName The name of the property to compare against
470     * @param mixed $operand The value to compare with
471     * @param bool $caseSensitive Whether the equality test should be done case-sensitive
472     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
473     */
474    public function equals($propertyName, $operand, $caseSensitive = true)
475    {
476        if (is_object($operand) || $caseSensitive) {
477            $comparison = $this->qomFactory->comparison(
478                $this->qomFactory->propertyValue($propertyName, $this->getSelectorName()),
479                QueryInterface::OPERATOR_EQUAL_TO,
480                $operand
481            );
482        } else {
483            $comparison = $this->qomFactory->comparison(
484                $this->qomFactory->lowerCase($this->qomFactory->propertyValue($propertyName, $this->getSelectorName())),
485                QueryInterface::OPERATOR_EQUAL_TO,
486                mb_strtolower($operand, \TYPO3\CMS\Extbase\Persistence\Generic\Query::CHARSET)
487            );
488        }
489        return $comparison;
490    }
491
492    /**
493     * Returns a like criterion used for matching objects against a query
494     *
495     * @param string $propertyName The name of the property to compare against
496     * @param mixed $operand The value to compare with
497     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
498     */
499    public function like($propertyName, $operand)
500    {
501        return $this->qomFactory->comparison(
502            $this->qomFactory->propertyValue($propertyName, $this->getSelectorName()),
503            QueryInterface::OPERATOR_LIKE,
504            $operand
505        );
506    }
507
508    /**
509     * Returns a "contains" criterion used for matching objects against a query.
510     * It matches if the multivalued property contains the given operand.
511     *
512     * @param string $propertyName The name of the (multivalued) property to compare against
513     * @param mixed $operand The value to compare with
514     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
515     */
516    public function contains($propertyName, $operand)
517    {
518        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_CONTAINS, $operand);
519    }
520
521    /**
522     * Returns an "in" criterion used for matching objects against a query. It
523     * matches if the property's value is contained in the multivalued operand.
524     *
525     * @param string $propertyName The name of the property to compare against
526     * @param mixed $operand The value to compare with, multivalued
527     * @throws Exception\UnexpectedTypeException
528     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
529     */
530    public function in($propertyName, $operand)
531    {
532        if (!\TYPO3\CMS\Extbase\Utility\TypeHandlingUtility::isValidTypeForMultiValueComparison($operand)) {
533            throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\UnexpectedTypeException('The "in" operator must be given a multivalued operand (array, ArrayAccess, Traversable).', 1264678095);
534        }
535        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_IN, $operand);
536    }
537
538    /**
539     * Returns a less than criterion used for matching objects against a query
540     *
541     * @param string $propertyName The name of the property to compare against
542     * @param mixed $operand The value to compare with
543     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
544     */
545    public function lessThan($propertyName, $operand)
546    {
547        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN, $operand);
548    }
549
550    /**
551     * Returns a less or equal than criterion used for matching objects against a query
552     *
553     * @param string $propertyName The name of the property to compare against
554     * @param mixed $operand The value to compare with
555     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
556     */
557    public function lessThanOrEqual($propertyName, $operand)
558    {
559        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_LESS_THAN_OR_EQUAL_TO, $operand);
560    }
561
562    /**
563     * Returns a greater than criterion used for matching objects against a query
564     *
565     * @param string $propertyName The name of the property to compare against
566     * @param mixed $operand The value to compare with
567     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
568     */
569    public function greaterThan($propertyName, $operand)
570    {
571        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN, $operand);
572    }
573
574    /**
575     * Returns a greater than or equal criterion used for matching objects against a query
576     *
577     * @param string $propertyName The name of the property to compare against
578     * @param mixed $operand The value to compare with
579     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\ComparisonInterface
580     */
581    public function greaterThanOrEqual($propertyName, $operand)
582    {
583        return $this->qomFactory->comparison($this->qomFactory->propertyValue($propertyName, $this->getSelectorName()), QueryInterface::OPERATOR_GREATER_THAN_OR_EQUAL_TO, $operand);
584    }
585
586    /**
587     * Returns a greater than or equal criterion used for matching objects against a query
588     *
589     * @param string $propertyName The name of the property to compare against
590     * @param mixed $operandLower The value of the lower boundary to compare against
591     * @param mixed $operandUpper The value of the upper boundary to compare against
592     * @return \TYPO3\CMS\Extbase\Persistence\Generic\Qom\AndInterface
593     * @throws \TYPO3\CMS\Extbase\Persistence\Generic\Exception\InvalidNumberOfConstraintsException
594     */
595    public function between($propertyName, $operandLower, $operandUpper)
596    {
597        return $this->logicalAnd(
598            $this->greaterThanOrEqual($propertyName, $operandLower),
599            $this->lessThanOrEqual($propertyName, $operandUpper)
600        );
601    }
602
603    /**
604     * @internal only to be used within Extbase, not part of TYPO3 Core API.
605     */
606    public function __wakeup()
607    {
608        $this->objectManager = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Extbase\Object\ObjectManager::class);
609        $this->persistenceManager = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\PersistenceManagerInterface::class);
610        $this->dataMapFactory = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapFactory::class);
611        $this->qomFactory = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory::class);
612    }
613
614    /**
615     * @return array
616     * @internal only to be used within Extbase, not part of TYPO3 Core API.
617     */
618    public function __sleep()
619    {
620        return ['type', 'source', 'constraint', 'statement', 'orderings', 'limit', 'offset', 'querySettings'];
621    }
622
623    /**
624     * Returns the query result count.
625     *
626     * @return int The query result count
627     */
628    public function count()
629    {
630        return $this->execute()->count();
631    }
632
633    /**
634     * Returns an "isEmpty" criterion used for matching objects against a query.
635     * It matches if the multivalued property contains no values or is NULL.
636     *
637     * @param string $propertyName The name of the multivalued property to compare against
638     * @throws Exception\NotImplementedException
639     */
640    public function isEmpty($propertyName)
641    {
642        throw new \TYPO3\CMS\Extbase\Persistence\Generic\Exception\NotImplementedException(__METHOD__, 1476122265);
643    }
644}
645