ILIAS  trunk Revision v11.0_alpha-1723-g8e69f309bab
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
DatabaseSearcher.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
33 
35 {
38  protected \ilDBInterface $db;
39 
40  public function __construct(
41  RessourceIDFactoryInterface $ressource_factory,
42  DatabasePathsParserFactoryInterface $paths_parser_factory,
43  \ilDBInterface $db
44  ) {
45  $this->ressource_factory = $ressource_factory;
46  $this->paths_parser_factory = $paths_parser_factory;
47  $this->db = $db;
48  }
49 
50  public function search(
51  ClauseInterface $clause,
52  ?int $limit,
53  ?int $offset,
54  FilterInterface ...$filters
55  ): \Generator {
56  $paths_parser = $this->paths_parser_factory->forSearch();
57  $where = $this->getClauseForQueryHaving($clause, $paths_parser);
58  $quoted_table_alias = $this->quoteIdentifier($paths_parser->getTableAliasForFilters());
59 
60  $query = $paths_parser->getSelectForQuery() . ' GROUP BY ' . $quoted_table_alias . '.rbac_id, ' .
61  $quoted_table_alias . '.obj_id, ' . $quoted_table_alias . '.obj_type HAVING ' . $where .
62  $this->getFiltersForQueryHaving($quoted_table_alias, ...$filters) .
63  ' ORDER BY rbac_id, obj_id, obj_type' . $this->getLimitAndOffsetForQuery($limit, $offset);
64 
65  foreach ($this->queryDB($query) as $row) {
66  yield $this->ressource_factory->ressourceID(
67  (int) $row['rbac_id'],
68  (int) $row['obj_id'],
69  (string) $row['obj_type']
70  );
71  }
72  }
73 
74  protected function getFiltersForQueryHaving(
75  string $quoted_table_alias,
76  FilterInterface ...$filters
77  ): string {
78  $filter_where = [];
79  foreach ($filters as $filter) {
80  $filter_values = [];
81  if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->objID())) {
82  $filter_values[] = $quoted_table_alias . '.rbac_id = ' . $val;
83  }
84  if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->subID())) {
85  $filter_values[] = $quoted_table_alias . '.obj_id = ' . $val;
86  }
87  if ($val = $this->getFilterValueForCondition($quoted_table_alias, $filter->type())) {
88  $filter_values[] = $quoted_table_alias . '.obj_type = ' . $val;
89  }
90  if (!empty($filter_values)) {
91  $filter_where[] = '(' . implode(' AND ', $filter_values) . ')';
92  }
93  }
94 
95  if (empty($filter_where)) {
96  return '';
97  }
98 
99  return ' AND (' . implode(' OR ', $filter_where) . ')';
100  }
101 
102  protected function getFilterValueForCondition(
103  string $quoted_table_alias,
104  string|int|Placeholder $value
105  ): string {
106  if (is_int($value)) {
107  return $this->quoteInteger($value);
108  }
109  if (is_string($value)) {
110  return $this->quoteText($value);
111  }
112 
113  switch ($value) {
114  case Placeholder::OBJ_ID:
115  return $quoted_table_alias . '.rbac_id';
116 
117  case Placeholder::SUB_ID:
118  return $quoted_table_alias . '.obj_id';
119 
120  case Placeholder::TYPE:
121  return $quoted_table_alias . '.obj_type';
122 
123  case Placeholder::ANY:
124  default:
125  return '';
126  }
127  }
128 
129  protected function getLimitAndOffsetForQuery(?int $limit, ?int $offset): string
130  {
131  $query_limit = '';
132  if (!is_null($limit) || !is_null($offset)) {
133  $limit = is_null($limit) ? PHP_INT_MAX : $limit;
134  $query_limit = ' LIMIT ' . $this->quoteInteger($limit);
135  }
136  $query_offset = '';
137  if (!is_null($offset)) {
138  $query_offset = ' OFFSET ' . $this->quoteInteger($offset);
139  }
140  return $query_limit . $query_offset;
141  }
142 
143  protected function getClauseForQueryHaving(
144  ClauseInterface $clause,
145  DatabasePathsParserInterface $paths_parser,
146  int $depth = 0
147  ): string {
148  if ($depth > 50) {
149  throw new \ilMDRepositoryException('Search clause is nested to deep.');
150  }
151 
152  if (!$clause->isJoin()) {
153  return $this->getBasicClauseForQueryWhere(
154  $clause->basicProperties(),
155  $clause->isNegated(),
156  $paths_parser
157  );
158  }
159 
160  $join_props = $clause->joinProperties();
161 
162  $sub_clauses_for_query = [];
163  foreach ($join_props->subClauses() as $sub_clause) {
164  $sub_clauses_for_query[] = $this->getClauseForQueryHaving($sub_clause, $paths_parser, $depth + 1);
165  }
166 
167  switch ($join_props->operator()) {
168  case Operator::AND:
169  $operator_for_query = 'AND';
170  break;
171 
172  case Operator::OR:
173  $operator_for_query = 'OR';
174  break;
175 
176  default:
177  throw new \ilMDRepositoryException('Invalid search operator.');
178  }
179 
180  $negation = '';
181  if ($clause->isNegated()) {
182  $negation = 'NOT ';
183  }
184 
185  return $negation . '(' . implode(' ' . $operator_for_query . ' ', $sub_clauses_for_query) . ')';
186  }
187 
188  protected function getBasicClauseForQueryWhere(
189  BasicPropertiesInterface $basic_props,
190  bool $is_clause_negated,
191  DatabasePathsParserInterface $paths_parser
192  ): string {
193  switch ($basic_props->mode()) {
194  case Mode::EQUALS:
195  $comparison = '= ' .
196  $this->quoteText($basic_props->value());
197  break;
198 
199  case Mode::CONTAINS:
200  $comparison = 'LIKE ' .
201  $this->quoteText('%' . $basic_props->value() . '%');
202  break;
203 
204  case Mode::STARTS_WITH:
205  $comparison = 'LIKE ' .
206  $this->quoteText($basic_props->value() . '%');
207  break;
208 
209  case Mode::ENDS_WITH:
210  $comparison = 'LIKE ' .
211  $this->quoteText('%' . $basic_props->value());
212  break;
213 
214  default:
215  throw new \ilMDRepositoryException('Invalid search mode.');
216  }
217 
218  $mode_negation = '';
219  if ($basic_props->isModeNegated()) {
220  $mode_negation = 'NOT ';
221  }
222  $clause_negation = '';
223  if ($is_clause_negated) {
224  $clause_negation = 'NOT ';
225  }
226 
227  $needs_join_to_base_table = $basic_props->value() === '' || $is_clause_negated;
228 
229  return $clause_negation . 'COUNT(CASE WHEN ' . $mode_negation .
230  $paths_parser->addPathAndGetColumn($basic_props->path(), $needs_join_to_base_table) .
231  ' ' . $comparison . ' THEN 1 END) > 0';
232  }
233 
234  protected function queryDB(string $query): \Generator
235  {
236  $result = $this->db->query($query);
237 
238  while ($row = $this->db->fetchAssoc($result)) {
239  yield $row;
240  }
241  }
242 
243  protected function quoteIdentifier(string $identifier): string
244  {
245  return $this->db->quoteIdentifier($identifier);
246  }
247 
248  protected function quoteText(string $text): string
249  {
250  return $this->db->quote($text, \ilDBConstants::T_TEXT);
251  }
252 
253  protected function quoteInteger(int $integer): string
254  {
255  return $this->db->quote($integer, \ilDBConstants::T_INTEGER);
256  }
257 }
addPathAndGetColumn(PathInterface $path, bool $force_join_to_base_table)
getBasicClauseForQueryWhere(BasicPropertiesInterface $basic_props, bool $is_clause_negated, DatabasePathsParserInterface $paths_parser)
getClauseForQueryHaving(ClauseInterface $clause, DatabasePathsParserInterface $paths_parser, int $depth=0)
search(ClauseInterface $clause, ?int $limit, ?int $offset, FilterInterface ... $filters)
getFiltersForQueryHaving(string $quoted_table_alias, FilterInterface ... $filters)
getFilterValueForCondition(string $quoted_table_alias, string|int|Placeholder $value)
__construct(RessourceIDFactoryInterface $ressource_factory, DatabasePathsParserFactoryInterface $paths_parser_factory, \ilDBInterface $db)