--- /dev/null
+<?php
+
+namespace Drupal\entity\QueryAccess;
+
+use Drupal\Core\Cache\Cache;
+use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
+use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
+
+/**
+ * Represents a group of query access conditions.
+ *
+ * Used by query access handlers for filtering lists of entities based on
+ * granted permissions.
+ *
+ * Examples:
+ * @code
+ * // Filter by node type and uid.
+ * $condition_group = new ConditionGroup();
+ * $condition_group->addCondition('type', ['article', 'page']);
+ * $condition_group->addCondition('uid', '1');
+ *
+ * // Filter by node type or status.
+ * $condition_group = new ConditionGroup('OR');
+ * $condition_group->addCondition('type', ['article', 'page']);
+ * $condition_group->addCondition('status', '1', '<>');
+ *
+ * // Nested condition groups: node type AND (uid OR status).
+ * $condition_group = new ConditionGroup();
+ * $condition_group->addCondition('type', ['article', 'page']);
+ * $condition_group->addCondition((new ConditionGroup('OR'))
+ * ->addCondition('uid', 1)
+ * ->addCondition('status', '1')
+ * );
+ * @endcode
+ */
+final class ConditionGroup implements \Countable, RefinableCacheableDependencyInterface {
+
+ use RefinableCacheableDependencyTrait;
+
+ /**
+ * The conjunction.
+ *
+ * @var string
+ */
+ protected $conjunction;
+
+ /**
+ * The conditions.
+ *
+ * @var \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[]
+ */
+ protected $conditions = [];
+
+ /**
+ * Whether the condition group is always FALSE.
+ *
+ * @var bool
+ */
+ protected $alwaysFalse = FALSE;
+
+ /**
+ * Constructs a new ConditionGroup object.
+ *
+ * @param string $conjunction
+ * The conjunction.
+ */
+ public function __construct($conjunction = 'AND') {
+ $this->conjunction = $conjunction;
+ }
+
+ /**
+ * Gets the conjunction.
+ *
+ * @return string
+ * The conjunction. Possible values: AND, OR.
+ */
+ public function getConjunction() {
+ return $this->conjunction;
+ }
+
+ /**
+ * Gets all conditions and nested condition groups.
+ *
+ * @return \Drupal\entity\QueryAccess\Condition[]|\Drupal\entity\QueryAccess\ConditionGroup[]
+ * The conditions, where each one is either a Condition or a nested
+ * ConditionGroup. Returned by reference, to allow callers to replace
+ * or remove conditions.
+ */
+ public function &getConditions() {
+ return $this->conditions;
+ }
+
+ /**
+ * Adds a condition.
+ *
+ * @param string|\Drupal\entity\QueryAccess\ConditionGroup $field
+ * Either a condition group (for nested AND/OR conditions), or a
+ * field name with an optional column name. E.g: 'uid', 'address.locality'.
+ * @param mixed $value
+ * The value.
+ * @param string $operator
+ * The operator.
+ * Possible values: =, <>, <, <=, >, >=, BETWEEN, NOT BETWEEN,
+ * IN, NOT IN, IS NULL, IS NOT NULL.
+ *
+ * @return $this
+ */
+ public function addCondition($field, $value = NULL, $operator = NULL) {
+ if ($field instanceof ConditionGroup) {
+ if ($field->count() === 1) {
+ // The condition group only has a single condition, merge it.
+ $this->conditions[] = reset($field->getConditions());
+ $this->addCacheTags($field->getCacheTags());
+ $this->addCacheContexts($field->getCacheContexts());
+ $this->mergeCacheMaxAge($field->getCacheMaxAge());
+ }
+ elseif ($field->count() > 1) {
+ $this->conditions[] = $field;
+ }
+ }
+ else {
+ $this->conditions[] = new Condition($field, $value, $operator);
+ }
+
+ return $this;
+ }
+
+ /**
+ * Gets whether the condition group is always FALSE.
+ *
+ * Used when the user doesn't have access to any entities, to ensure that a
+ * query returns no results.
+ *
+ * @return bool
+ * Whether the condition group is always FALSE.
+ */
+ public function isAlwaysFalse() {
+ return $this->alwaysFalse;
+ }
+
+ /**
+ * Sets whether the condition group should always be FALSE.
+ *
+ * @param bool $always_false
+ * Whether the condition group should always be FALSE.
+ *
+ * @return $this
+ */
+ public function alwaysFalse($always_false = TRUE) {
+ $this->alwaysFalse = $always_false;
+ return $this;
+ }
+
+ /**
+ * Clones the contained conditions when the condition group is cloned.
+ */
+ public function __clone() {
+ foreach ($this->conditions as $i => $condition) {
+ $this->conditions[$i] = clone $condition;
+ }
+ }
+
+ /**
+ * Gets the string representation of the condition group.
+ *
+ * @return string
+ * The string representation of the condition group.
+ */
+ public function __toString() {
+ // Special case for a single, nested condition group:
+ if (count($this->conditions) == 1) {
+ return (string) reset($this->conditions);
+ }
+ $lines = [];
+ foreach ($this->conditions as $condition) {
+ $lines[] = str_replace("\n", "\n ", (string) $condition);
+ }
+ return $lines ? "(\n " . implode("\n {$this->conjunction}\n ", $lines) . "\n)" : '';
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function count() {
+ return count($this->conditions);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheTags() {
+ $tags = $this->cacheTags;
+ foreach ($this->conditions as $condition) {
+ if ($condition instanceof ConditionGroup) {
+ $tags = array_merge($tags, $condition->getCacheTags());
+ }
+ }
+ return Cache::mergeTags($tags, []);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheContexts() {
+ $cache_contexts = $this->cacheContexts;
+ foreach ($this->conditions as $condition) {
+ if ($condition instanceof ConditionGroup) {
+ $cache_contexts = array_merge($cache_contexts, $condition->getCacheContexts());
+ }
+ }
+ return Cache::mergeContexts($cache_contexts);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getCacheMaxAge() {
+ $max_age = $this->cacheMaxAge;
+ foreach ($this->conditions as $condition) {
+ if ($condition instanceof ConditionGroup) {
+ $max_age = Cache::mergeMaxAges($max_age, $condition->getCacheMaxAge());
+ }
+ }
+ return $max_age;
+ }
+
+}