123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410 |
- <?php
- namespace Drupal\Core\Database\Query;
- use Drupal\Core\Database\Connection;
- use Drupal\Core\Database\InvalidQueryException;
- /**
- * Generic class for a series of conditions in a query.
- */
- class Condition implements ConditionInterface, \Countable {
- /**
- * Provides a map of condition operators to condition operator options.
- */
- protected static $conditionOperatorMap = [
- 'BETWEEN' => ['delimiter' => ' AND '],
- 'NOT BETWEEN' => ['delimiter' => ' AND '],
- 'IN' => ['delimiter' => ', ', 'prefix' => '(', 'postfix' => ')'],
- 'NOT IN' => ['delimiter' => ', ', 'prefix' => '(', 'postfix' => ')'],
- 'IS NULL' => ['use_value' => FALSE],
- 'IS NOT NULL' => ['use_value' => FALSE],
- // Use backslash for escaping wildcard characters.
- 'LIKE' => ['postfix' => " ESCAPE '\\\\'"],
- 'NOT LIKE' => ['postfix' => " ESCAPE '\\\\'"],
- // Exists expects an already bracketed subquery as right hand part. Do
- // not define additional brackets.
- 'EXISTS' => [],
- 'NOT EXISTS' => [],
- // These ones are here for performance reasons.
- '=' => [],
- '<' => [],
- '>' => [],
- '>=' => [],
- '<=' => [],
- ];
- /**
- * Array of conditions.
- *
- * @var array
- */
- protected $conditions = [];
- /**
- * Array of arguments.
- *
- * @var array
- */
- protected $arguments = [];
- /**
- * Whether the conditions have been changed.
- *
- * TRUE if the condition has been changed since the last compile.
- * FALSE if the condition has been compiled and not changed.
- *
- * @var bool
- */
- protected $changed = TRUE;
- /**
- * The identifier of the query placeholder this condition has been compiled against.
- */
- protected $queryPlaceholderIdentifier;
- /**
- * Contains the string version of the Condition.
- *
- * @var string
- */
- protected $stringVersion;
- /**
- * Constructs a Condition object.
- *
- * @param string $conjunction
- * The operator to use to combine conditions: 'AND' or 'OR'.
- */
- public function __construct($conjunction) {
- $this->conditions['#conjunction'] = $conjunction;
- }
- /**
- * Implements Countable::count().
- *
- * Returns the size of this conditional. The size of the conditional is the
- * size of its conditional array minus one, because one element is the
- * conjunction.
- */
- public function count() {
- return count($this->conditions) - 1;
- }
- /**
- * {@inheritdoc}
- */
- public function condition($field, $value = NULL, $operator = '=') {
- if (empty($operator)) {
- $operator = '=';
- }
- if (empty($value) && is_array($value)) {
- throw new InvalidQueryException(sprintf("Query condition '%s %s ()' cannot be empty.", $field, $operator));
- }
- $this->conditions[] = [
- 'field' => $field,
- 'value' => $value,
- 'operator' => $operator,
- ];
- $this->changed = TRUE;
- return $this;
- }
- /**
- * {@inheritdoc}
- */
- public function where($snippet, $args = []) {
- $this->conditions[] = [
- 'field' => $snippet,
- 'value' => $args,
- 'operator' => NULL,
- ];
- $this->changed = TRUE;
- return $this;
- }
- /**
- * {@inheritdoc}
- */
- public function isNull($field) {
- return $this->condition($field, NULL, 'IS NULL');
- }
- /**
- * {@inheritdoc}
- */
- public function isNotNull($field) {
- return $this->condition($field, NULL, 'IS NOT NULL');
- }
- /**
- * {@inheritdoc}
- */
- public function exists(SelectInterface $select) {
- return $this->condition('', $select, 'EXISTS');
- }
- /**
- * {@inheritdoc}
- */
- public function notExists(SelectInterface $select) {
- return $this->condition('', $select, 'NOT EXISTS');
- }
- /**
- * {@inheritdoc}
- */
- public function &conditions() {
- return $this->conditions;
- }
- /**
- * {@inheritdoc}
- */
- public function arguments() {
- // If the caller forgot to call compile() first, refuse to run.
- if ($this->changed) {
- return NULL;
- }
- return $this->arguments;
- }
- /**
- * {@inheritdoc}
- */
- public function compile(Connection $connection, PlaceholderInterface $queryPlaceholder) {
- // Re-compile if this condition changed or if we are compiled against a
- // different query placeholder object.
- if ($this->changed || isset($this->queryPlaceholderIdentifier) && ($this->queryPlaceholderIdentifier != $queryPlaceholder->uniqueIdentifier())) {
- $this->queryPlaceholderIdentifier = $queryPlaceholder->uniqueIdentifier();
- $condition_fragments = [];
- $arguments = [];
- $conditions = $this->conditions;
- $conjunction = $conditions['#conjunction'];
- unset($conditions['#conjunction']);
- foreach ($conditions as $condition) {
- // Process field.
- if ($condition['field'] instanceof ConditionInterface) {
- // Left hand part is a structured condition or a subquery. Compile,
- // put brackets around it (if it is a query), and collect any
- // arguments.
- $condition['field']->compile($connection, $queryPlaceholder);
- $field_fragment = (string) $condition['field'];
- if ($condition['field'] instanceof SelectInterface) {
- $field_fragment = '(' . $field_fragment . ')';
- }
- $arguments += $condition['field']->arguments();
- // If the operator and value were not passed in to the
- // @see ConditionInterface::condition() method (and thus have the
- // default value as defined over there) it is assumed to be a valid
- // condition on its own: ignore the operator and value parts.
- $ignore_operator = $condition['operator'] === '=' && $condition['value'] === NULL;
- }
- elseif (!isset($condition['operator'])) {
- // Left hand part is a literal string added with the
- // @see ConditionInterface::where() method. Put brackets around
- // the snippet and collect the arguments from the value part.
- // Also ignore the operator and value parts.
- $field_fragment = '(' . $condition['field'] . ')';
- $arguments += $condition['value'];
- $ignore_operator = TRUE;
- }
- else {
- // Left hand part is a normal field. Add it as is.
- $field_fragment = $connection->escapeField($condition['field']);
- $ignore_operator = FALSE;
- }
- // Process operator.
- if ($ignore_operator) {
- $operator = ['operator' => '', 'use_value' => FALSE];
- }
- else {
- // Remove potentially dangerous characters.
- // If something passed in an invalid character stop early, so we
- // don't rely on a broken SQL statement when we would just replace
- // those characters.
- if (stripos($condition['operator'], 'UNION') !== FALSE || strpbrk($condition['operator'], '[-\'"();') !== FALSE) {
- $this->changed = TRUE;
- $this->arguments = [];
- // Provide a string which will result into an empty query result.
- $this->stringVersion = '( AND 1 = 0 )';
- // Conceptually throwing an exception caused by user input is bad
- // as you result into a WSOD, which depending on your webserver
- // configuration can result into the assumption that your site is
- // broken.
- // On top of that the database API relies on __toString() which
- // does not allow to throw exceptions.
- trigger_error('Invalid characters in query operator: ' . $condition['operator'], E_USER_ERROR);
- return;
- }
- // For simplicity, we convert all operators to a data structure to
- // allow to specify a prefix, a delimiter and such. Find the
- // associated data structure by first doing a database specific
- // lookup, followed by a specification according to the SQL standard.
- $operator = $connection->mapConditionOperator($condition['operator']);
- if (!isset($operator)) {
- $operator = $this->mapConditionOperator($condition['operator']);
- }
- $operator += ['operator' => $condition['operator']];
- }
- // Add defaults.
- $operator += [
- 'prefix' => '',
- 'postfix' => '',
- 'delimiter' => '',
- 'use_value' => TRUE,
- ];
- $operator_fragment = $operator['operator'];
- // Process value.
- $value_fragment = '';
- if ($operator['use_value']) {
- // For simplicity, we first convert to an array, so that we can handle
- // the single and multi value cases the same.
- if (!is_array($condition['value'])) {
- if ($condition['value'] instanceof SelectInterface && ($operator['operator'] === 'IN' || $operator['operator'] === 'NOT IN')) {
- // Special case: IN is followed by a single select query instead
- // of a set of values: unset prefix and postfix to prevent double
- // brackets.
- $operator['prefix'] = '';
- $operator['postfix'] = '';
- }
- $condition['value'] = [$condition['value']];
- }
- // Process all individual values.
- $value_fragment = [];
- foreach ($condition['value'] as $value) {
- if ($value instanceof SelectInterface) {
- // Right hand part is a subquery. Compile, put brackets around it
- // and collect any arguments.
- $value->compile($connection, $queryPlaceholder);
- $value_fragment[] = '(' . (string) $value . ')';
- $arguments += $value->arguments();
- }
- else {
- // Right hand part is a normal value. Replace the value with a
- // placeholder and add the value as an argument.
- $placeholder = ':db_condition_placeholder_' . $queryPlaceholder->nextPlaceholder();
- $value_fragment[] = $placeholder;
- $arguments[$placeholder] = $value;
- }
- }
- $value_fragment = $operator['prefix'] . implode($operator['delimiter'], $value_fragment) . $operator['postfix'];
- }
- // Concatenate the left hand part, operator and right hand part.
- $condition_fragments[] = trim(implode(' ', [$field_fragment, $operator_fragment, $value_fragment]));
- }
- // Concatenate all conditions using the conjunction and brackets around
- // the individual conditions to assure the proper evaluation order.
- $this->stringVersion = count($condition_fragments) > 1 ? '(' . implode(") $conjunction (", $condition_fragments) . ')' : implode($condition_fragments);
- $this->arguments = $arguments;
- $this->changed = FALSE;
- }
- }
- /**
- * {@inheritdoc}
- */
- public function compiled() {
- return !$this->changed;
- }
- /**
- * Implements PHP magic __toString method to convert the conditions to string.
- *
- * @return string
- * A string version of the conditions.
- */
- public function __toString() {
- // If the caller forgot to call compile() first, refuse to run.
- if ($this->changed) {
- return '';
- }
- return $this->stringVersion;
- }
- /**
- * PHP magic __clone() method.
- *
- * Only copies fields that implement Drupal\Core\Database\Query\ConditionInterface. Also sets
- * $this->changed to TRUE.
- */
- public function __clone() {
- $this->changed = TRUE;
- foreach ($this->conditions as $key => $condition) {
- if ($key !== '#conjunction') {
- if ($condition['field'] instanceof ConditionInterface) {
- $this->conditions[$key]['field'] = clone($condition['field']);
- }
- if ($condition['value'] instanceof SelectInterface) {
- $this->conditions[$key]['value'] = clone($condition['value']);
- }
- }
- }
- }
- /**
- * Gets any special processing requirements for the condition operator.
- *
- * Some condition types require special processing, such as IN, because
- * the value data they pass in is not a simple value. This is a simple
- * overridable lookup function.
- *
- * @param string $operator
- * The condition operator, such as "IN", "BETWEEN", etc. Case-sensitive.
- *
- * @return array
- * The extra handling directives for the specified operator or an empty
- * array if there are no extra handling directives.
- */
- protected function mapConditionOperator($operator) {
- if (isset(static::$conditionOperatorMap[$operator])) {
- $return = static::$conditionOperatorMap[$operator];
- }
- else {
- // We need to upper case because PHP index matches are case sensitive but
- // do not need the more expensive mb_strtoupper() because SQL statements
- // are ASCII.
- $operator = strtoupper($operator);
- $return = isset(static::$conditionOperatorMap[$operator]) ? static::$conditionOperatorMap[$operator] : [];
- }
- $return += ['operator' => $operator];
- return $return;
- }
- /**
- * {@inheritdoc}
- */
- public function conditionGroupFactory($conjunction = 'AND') {
- return new Condition($conjunction);
- }
- /**
- * {@inheritdoc}
- */
- public function andConditionGroup() {
- return $this->conditionGroupFactory('AND');
- }
- /**
- * {@inheritdoc}
- */
- public function orConditionGroup() {
- return $this->conditionGroupFactory('OR');
- }
- }
|