ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
DatabaseHistoryRepository.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
23use DateTimeImmutable;
35use ilObjUser;
36use Exception;
37use Closure;
38
40{
41 use DeriveFieldTypes;
42
43 public function __construct(
44 private readonly string $id,
45 private readonly DocumentRepositoryMeta $document_meta,
46 private readonly ilDBInterface $database,
47 private readonly UserAction $action
48 ) {
49 }
50
51 public function acceptDocument(ilObjUser $user, Document $document): void
52 {
53 $this->trackUser($user->getId(), $document, $this->documentVersion($document)->except(
54 fn() => new Ok($this->addDocumentVersion($document))
55 )->value());
56 }
57
58 public function alreadyAccepted(ilObjUser $user, Document $document): bool
59 {
60 $versions = $this->versionTable();
61 $tracking = $this->trackingTable();
62 $hash = $this->database->quote($this->documentHash($document), ilDBConstants::T_TEXT);
63 $user = $this->database->quote($user->getId(), ilDBConstants::T_INTEGER);
64 $doc_id = $this->database->quote($document->id(), ilDBConstants::T_INTEGER);
65
66 return null !== $this->database->fetchAssoc($this->database->query(
67 "SELECT 1 FROM $versions INNER JOIN $tracking ON $versions.id = $tracking.tosv_id WHERE $versions.doc_id = $doc_id AND hash = $hash AND usr_id = $user AND "
68 . $this->document_meta->exists('doc_id')
69 ));
70 }
71
72 public function acceptedVersion(ilObjUser $user): Result
73 {
74 $versions = $this->versionTable();
75 $tracking = $this->trackingTable();
76 $user_id = $this->database->quote($user->getId(), ilDBConstants::T_INTEGER);
77 $provider = $this->database->quote($this->id, ilDBConstants::T_TEXT);
78
79 $result = $this->database->fetchAssoc($this->database->query(
80 "SELECT $versions.* from $versions inner join $tracking on $versions.id = $tracking.tosv_id where usr_id = $user_id AND $versions.provider = $provider ORDER BY $tracking.ts DESC"
81 ));
82
83 if ($result === null) {
84 return new Error('Not found.');
85 }
86
87 return new Ok(new DocumentContent($result['type'], $result['title'] ?? '', $result['text'] ?? ''));
88 }
89
91 {
92 $tracking = $this->trackingTable();
93 $version = $this->versionTable();
94
95 $this->database->setLimit(1);
96 $result = $this->database->fetchAssoc($this->database->queryF(
97 "SELECT doc_id FROM $tracking INNER JOIN $version ON $tracking.tosv_id = $version.id WHERE provider = %s AND usr_id = %s ORDER BY $tracking.ts DESC",
99 [$this->id, $user->getId()]
100 ));
101
102 return $result ? $this->document_meta->find((int) $result['doc_id']) : new Error('No document found.');
103 }
104
109 public function all(array $filter = [], array $order_by = [], int $offset = 0, ?int $limit = null): array
110 {
111 $tracking = $this->trackingTable();
112 $version = $this->versionTable();
113 $documents = $this->document_meta->documentTable();
114 $provider = $this->database->quote($this->id, ilDBConstants::T_TEXT);
115
116 [$filter, $join] = $this->filterAndJoin($filter, $order_by);
117
118 $order_by = $this->mapKeys(fn($field) => match ($field) {
119 'created' => "$tracking.ts",
120 'document' => "$version.text",
121 'login' => "usr_data.login",
122 'firstname' => "usr_data.firstname",
123 'lastname' => "usr_data.lastname",
124 }, $order_by);
125
126 $rows = $this->database->fetchAll($this->database->query(
127 "SELECT $documents.*, $tracking.*, $version.text as old_text, $version.title as old_title, $version.type as old_type, $tracking.ts as ts FROM $tracking INNER JOIN $version ON $tracking.tosv_id = $version.id LEFT JOIN $documents ON $documents.id = $version.doc_id $join WHERE $filter AND $version.provider = $provider " .
128 $this->orderSqlFromArray($order_by) .
129 (null === $limit ? '' : ' LIMIT ' . $offset . ', ' . $limit)
130 ));
131
132 return array_map($this->recordFromRow(...), $rows);
133 }
134
138 public function countAll(array $filter = []): int
139 {
140 $tracking = $this->trackingTable();
141 $version = $this->versionTable();
142 $documents = $this->document_meta->documentTable();
143 $provider = $this->database->quote($this->id, ilDBConstants::T_TEXT);
144
145 [$filter, $join] = $this->filterAndJoin($filter);
146
147 return (int) $this->database->fetchAssoc($this->database->query(
148 "SELECT COUNT(1) as count FROM $tracking INNER JOIN $version ON $tracking.tosv_id = $version.id INNER JOIN $documents ON $version.doc_id = $documents.id AND $version.provider = $provider " .
149 "$join WHERE $filter"
150 ))['count'];
151 }
152
156 private function recordFromRow(array $row): History
157 {
158 return new History(
159 isset($row['id']) ? $this->document_meta->documentFromRow($row, []) : null,
160 new Edit((int) $row['usr_id'], new DateTimeImmutable('@' . $row['ts'])),
161 array_map(fn(array $criterion) => new CriterionContent($criterion['id'], $criterion['value']), json_decode($row['criteria'], true) ?: []),
162 new DocumentContent($row['old_type'], $row['old_title'] ?? '', $row['old_text'] ?? '')
163 );
164 }
165
166 private function documentHash(Document $document): string
167 {
168 return md5($document->content()->value());
169 }
170
174 private function documentVersion(Document $document): Result
175 {
176 $versions = $this->versionTable();
177 $documents = $this->document_meta->documentTable();
178 $hash = $this->database->quote($this->documentHash($document), ilDBConstants::T_TEXT);
179 $id = $this->database->quote($document->id(), ilDBConstants::T_INTEGER);
180 $result = $this->database->fetchAssoc($this->database->query("SELECT id FROM $versions WHERE doc_id = $id AND hash = $hash AND " . $this->document_meta->exists('doc_id')));
181
182 return $result ?
183 new Ok((int) $result['id']) :
184 new Error('Version not found');
185 }
186
187 private function trackUser(int $user, Document $document, int $version_id): void
188 {
189 $this->database->insert($this->trackingTable(), $this->deriveFieldTypes([
190 'tosv_id' => $version_id,
191 'usr_id' => $user,
192 'ts' => $this->action->modifiedNow()->time(),
193 'criteria' => json_encode(array_map(fn($x) => [
194 'id' => $x->content()->type(),
195 'value' => $x->content()->arguments(),
196 ], $document->criteria())),
197 ]));
198 }
199
200 private function addDocumentVersion(Document $document): int
201 {
202 $id = $this->database->nextId($this->versionTable());
203 $this->database->insert($this->versionTable(), $this->deriveFieldTypes([
204 'id' => $id,
205 'text' => $document->content()->value(),
206 'hash' => md5($document->content()->value()),
207 'ts' => $this->action->modifiedNow()->time(),
208 'doc_id' => $document->id(),
209 'title' => $document->content()->title(),
210 'provider' => $this->id,
211 ]));
212
213 return $id;
214 }
215
216 private function versionTable(): string
217 {
218 return 'ldoc_versions';
219 }
220
221 private function trackingTable(): string
222 {
223 return 'ldoc_acceptance_track';
224 }
225
226 private function error(string $message): void
227 {
228 throw new Exception($message);
229 }
230
234 private function orderSqlFromArray(array $order_by): string
235 {
236 $valid_field = fn(string $field) => (
237 preg_match('/^([[:alnum:]_]+\.)?[[:alnum:]_]+$/i', $field) !== 1 &&
238 $this->error('Invalid field name given: ' . print_r($field, true))
239 );
240 $valid_direction = fn(string $direction) => (
241 !in_array(strtolower($direction), ['asc', 'desc'], true) &&
242 $this->error('Invalid order direction given, only asc and desc allowed, given: ' . print_r($direction, true))
243 );
244
245 array_map($valid_direction, $order_by);
246 array_map($valid_field, array_keys($order_by));
247
248 $order_by = join(', ', array_map(
249 fn(string $field, string $direction) => join(' ', [$field, $direction]),
250 array_keys($order_by),
251 $order_by,
252 ));
253
254 return $order_by !== '' ? ' ORDER BY ' . $order_by : '';
255 }
256
266 private function mapKeys(Closure $proc, array $array): array
267 {
268 return array_combine(
269 array_map($proc, array_keys($array)),
270 array_values($array)
271 );
272 }
273
280 private function filterQuery(array $filter, Closure $filter_to_query): string
281 {
282 $query = '1';
283 foreach ($filter as $key => $value) {
284 $query .= ' AND ' . $filter_to_query($key, $value);
285 }
286
287 return $query;
288 }
289
295 private function filterAndJoin(array $filter, array $order_by = []): array
296 {
297 $tracking = $this->trackingTable();
298
299 $join = array_intersect(['login', 'firstname', 'lastname'], array_keys($order_by)) !== [] || isset($filter['query']) ?
300 " LEFT JOIN usr_data ON $tracking.usr_id = usr_data.usr_id " :
301 '';
302
303 $filter = $this->filterQuery($filter, fn($name, $value) => match ($name) {
304 'start' => "$tracking.ts >= " . $this->database->quote($value, ilDBConstants::T_DATETIME),
305 'end' => "$tracking.ts <= " . $this->database->quote($value, ilDBConstants::T_DATETIME),
306 'query' => '(' . join(' OR ', array_map(
307 fn($s, $v) => 'usr_data.' . $s . ' LIKE ' . $v,
308 ['login', 'email', 'firstname', 'lastname'],
309 array_fill(0, 4, $this->database->quote('%' . $value . '%', ilDBConstants::T_TEXT))
310 )) . ')',
311 });
312
313 return [$filter, $join];
314 }
315}
$version
Definition: plugin.php:24
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
A result encapsulates a value or an error and simplifies the handling of those.
Definition: Ok.php:31
trackUser(int $user, Document $document, int $version_id)
__construct(private readonly string $id, private readonly DocumentRepositoryMeta $document_meta, private readonly ilDBInterface $database, private readonly UserAction $action)
filterQuery(array $filter, Closure $filter_to_query)
@template A
all(array $filter=[], array $order_by=[], int $offset=0, ?int $limit=null)
mapKeys(Closure $proc, array $array)
@template A @template B @template C
Class ilDBConstants.
User class.
A result encapsulates a value or an error and simplifies the handling of those.
Definition: Result.php:29
Interface ilDBInterface.
$provider
Definition: ltitoken.php:80
$message
Definition: xapiexit.php:31