]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/DynamicFieldsHandle.php
Use same permissions in dynamic and core fields
[galette.git] / galette / lib / Galette / Entity / DynamicFieldsHandle.php
1 <?php
2
3 /**
4 * Copyright © 2003-2024 The Galette Team
5 *
6 * This file is part of Galette (https://galette.eu).
7 *
8 * Galette is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * Galette is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * You should have received a copy of the GNU General Public License
19 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
20 */
21
22 namespace Galette\Entity;
23
24 use ArrayObject;
25 use Galette\DynamicFields\File;
26 use Galette\DynamicFields\Separator;
27 use Laminas\Db\ResultSet\ResultSet;
28 use Throwable;
29 use Analog\Analog;
30 use Laminas\Db\Adapter\Driver\StatementInterface;
31 use Galette\Core\Db;
32 use Galette\Core\Login;
33 use Galette\Core\Authentication;
34 use Galette\DynamicFields\DynamicField;
35 use Galette\Repository\DynamicFieldsSet;
36
37 /**
38 * Dynamic fields handle, aggregating field descriptors and values
39 *
40 * @author Johan Cwiklinski <johan@x-tnd.be>
41 */
42
43 class DynamicFieldsHandle
44 {
45 public const TABLE = 'dynamic_fields';
46
47 /** @var DynamicField[] */
48 private array $dynamic_fields = [];
49 /** @var array<int, array<int, mixed>> */
50 private array $current_values = [];
51 private string $form_name;
52 private ?int $item_id;
53
54 /** @var array<string> */
55 private array $errors = array();
56
57 private Db $zdb;
58 private Login $login;
59
60 private StatementInterface $insert_stmt;
61 private StatementInterface $update_stmt;
62 private StatementInterface $delete_stmt;
63
64 private bool $has_changed = false;
65
66 /**
67 * Default constructor
68 *
69 * @param Db $zdb Database instance
70 * @param Login $login Login instance
71 * @param ?object $instance Object instance
72 */
73 public function __construct(Db $zdb, Login $login, object $instance = null)
74 {
75 $this->zdb = $zdb;
76 $this->login = $login;
77 if ($instance !== null) {
78 $this->load($instance);
79 }
80 }
81
82 /**
83 * Load dynamic fields values for specified object
84 *
85 * @param object $object Object instance
86 *
87 * @return bool
88 */
89 public function load(object $object): bool
90 {
91 $this->form_name = $object->getFormName();
92
93 try {
94 $this->item_id = $object->id;
95 $fields = new DynamicFieldsSet($this->zdb, $this->login);
96 $this->dynamic_fields = $fields->getList($this->form_name);
97
98 $results = $this->getCurrentFields();
99
100 if ($results->count() > 0) {
101 foreach ($results as $f) {
102 if (isset($this->dynamic_fields[$f->{DynamicField::PK}])) {
103 $field = $this->dynamic_fields[$f->{DynamicField::PK}];
104 if ($field->hasFixedValues()) {
105 $choices = $field->getValues();
106 if (!isset($choices[$f->field_val])) {
107 if ($idx = array_search($f->field_val, $choices)) {
108 //text has been stored (from CSV import?), but we want the index
109 $f->text_val = $f->field_val;
110 $f->field_val = $idx;
111 } else {
112 //something went wrong here :(
113 Analog::log(
114 'Dynamic choice value "' . $f->field_val . '" does not exists!',
115 Analog::WARNING
116 );
117 $f->text_val = $f->field_val;
118 }
119 } else {
120 $f->text_val = $choices[$f->field_val];
121 }
122 }
123 $this->current_values[$f->{DynamicField::PK}][] = array_filter(
124 (array)$f,
125 static function ($k) {
126 return $k != DynamicField::PK;
127 },
128 ARRAY_FILTER_USE_KEY
129 );
130 } else {
131 Analog::log(
132 'Dynamic values found for ' . get_class($object) . ' #' . $this->item_id .
133 '; but no dynamic field configured!',
134 Analog::WARNING
135 );
136 }
137 }
138 return true;
139 } else {
140 return false;
141 }
142 } catch (Throwable $e) {
143 Analog::log(
144 __METHOD__ . ' | ' . $e->getMessage(),
145 Analog::WARNING
146 );
147 throw $e;
148 }
149 }
150
151 /**
152 * Get errors
153 *
154 * @return array<string>
155 */
156 public function getErrors(): array
157 {
158 return $this->errors;
159 }
160
161 /**
162 * Get fields
163 *
164 * @return array<int, DynamicField>
165 */
166 public function getFields(): array
167 {
168 return $this->dynamic_fields;
169 }
170
171 /**
172 * Get fields for search pages
173 *
174 * @return array<int, DynamicField>
175 */
176 public function getSearchFields(): array
177 {
178 $dynamics = $this->dynamic_fields;
179
180 foreach ($dynamics as $key => $field) {
181 if ($field instanceof Separator || $field instanceof File) {
182 unset($dynamics[$key]);
183 }
184 }
185
186 return $dynamics;
187 }
188
189 /**
190 * Get values
191 *
192 * @param integer $field Field ID
193 *
194 * @return array<int, array<string, mixed>>
195 */
196 public function getValues(int $field): array
197 {
198 if (!isset($this->current_values[$field])) {
199 $this->current_values[$field][] = [
200 'item_id' => $this->item_id,
201 'field_form' => $this->dynamic_fields[$field]->getForm(),
202 'val_index' => 1,
203 'field_val' => '',
204 'is_new' => true
205 ];
206 }
207 return $this->current_values[$field];
208 }
209
210 /**
211 * Set field value
212 *
213 * @param ?integer $item Item ID
214 * @param integer $field Field ID
215 * @param integer $index Value index
216 * @param string|int $value Value
217 *
218 * @return void
219 */
220 public function setValue(?int $item, int $field, int $index, string|int $value): void
221 {
222 $idx = $index - 1;
223 $input = [
224 'item_id' => $item,
225 'field_form' => $this->dynamic_fields[$field]->getForm(),
226 'val_index' => $index,
227 'field_val' => $value,
228 ];
229
230 if (!isset($this->current_values[$field][$idx])) {
231 $input['is_new'] = true;
232 }
233
234 $this->current_values[$field][$idx] = $input;
235 }
236
237 /**
238 * Unset field value
239 *
240 * @param integer $field Field ID
241 * @param integer $index Value index
242 *
243 * @return void
244 */
245 public function unsetValue(int $field, int $index): void
246 {
247 $idx = $index - 1;
248 if (isset($this->current_values[$field][$idx])) {
249 unset($this->current_values[$field][$idx]);
250 }
251 }
252
253 /**
254 * Store values
255 *
256 * @param ?integer $item_id Current item id to use (will be used if current item_id is 0)
257 * @param boolean $transaction True if a transaction already exists
258 *
259 * @return boolean
260 */
261 public function storeValues(int $item_id = null, bool $transaction = false): bool
262 {
263 try {
264 if ($item_id !== null && ($this->item_id === null || $this->item_id === 0)) {
265 $this->item_id = $item_id;
266 }
267 if (!$transaction) {
268 $this->zdb->connection->beginTransaction();
269 }
270
271 $this->handleRemovals();
272
273 foreach ($this->current_values as $field_id => $values) {
274 foreach ($values as $value) {
275 $value[DynamicField::PK] = $field_id;
276 if ($value['item_id'] == 0) {
277 $value['item_id'] = $this->item_id;
278 }
279
280 if (isset($value['is_new'])) {
281 unset($value['is_new']);
282 $this->getInsertStatement()->execute($value);
283 $this->has_changed = true;
284 } else {
285 $params = [
286 'field_val' => $value['field_val'],
287 'val_index' => $value['val_index'],
288 'item_id' => $value['item_id'],
289 'field_id' => $value['field_id'],
290 'field_form' => $value['field_form'],
291 'old_val_index' => $value['old_val_index'] ?? $value['val_index'] //:old_val_index
292 ];
293 $this->getUpdateStatement()->execute($params);
294 $this->has_changed = true;
295 }
296 }
297 }
298
299 if (!$transaction) {
300 $this->zdb->connection->commit();
301 }
302 return true;
303 } catch (Throwable $e) {
304 if (!$transaction) {
305 $this->zdb->connection->rollBack();
306 }
307 Analog::log(
308 'An error occurred storing dynamic field. Form name: ' . $this->form_name .
309 ' | Error was: ' . $e->getMessage(),
310 Analog::ERROR
311 );
312 throw $e;
313 } finally {
314 unset(
315 $this->update_stmt,
316 $this->insert_stmt
317 );
318 }
319 }
320
321 /**
322 * Get (and prepare if not done yet) insert statement
323 *
324 * @return StatementInterface
325 */
326 private function getInsertStatement(): StatementInterface
327 {
328 if (!isset($this->insert_stmt)) {
329 $insert = $this->zdb->insert(self::TABLE);
330 $insert->values([
331 'item_id' => ':item_id',
332 'field_id' => ':field_id',
333 'field_form' => ':field_form',
334 'val_index' => ':val_index',
335 'field_val' => ':field_val'
336 ]);
337 $this->insert_stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
338 }
339 return $this->insert_stmt;
340 }
341
342 /**
343 * Get (and prepare if not done yet) update statement
344 *
345 * @return StatementInterface
346 */
347 private function getUpdateStatement(): StatementInterface
348 {
349 if (!isset($this->update_stmt)) {
350 $update = $this->zdb->update(self::TABLE);
351 $update->set([
352 'field_val' => ':field_val',
353 'val_index' => ':val_index'
354 ])->where([
355 'item_id' => ':item_id',
356 'field_id' => ':field_id',
357 'field_form' => ':field_form',
358 'val_index' => ':old_val_index'
359 ]);
360 $this->update_stmt = $this->zdb->sql->prepareStatementForSqlObject($update);
361 }
362 return $this->update_stmt;
363 }
364
365 /**
366 * Handle values that have been removed
367 *
368 * @return void
369 */
370 private function handleRemovals(): void
371 {
372 $fields = new DynamicFieldsSet($this->zdb, $this->login);
373 $this->dynamic_fields = $fields->getList($this->form_name);
374
375 $results = $this->getCurrentFields();
376
377 $fromdb = [];
378 if ($results->count() > 0) {
379 foreach ($results as $result) {
380 $fromdb[$result->field_id . '_' . $result->val_index] = [
381 'item_id' => $this->item_id,
382 'field_form' => $this->form_name,
383 'field_id' => $result->field_id,
384 'val_index' => $result->val_index
385 ];
386 }
387 }
388
389 if (!count($fromdb)) {
390 //no entry in database, nothing to do.
391 return;
392 }
393
394 foreach ($this->current_values as $field_id => $values) {
395 foreach ($values as $value) {
396 $key = $field_id . '_' . $value['val_index'];
397 if (isset($fromdb[$key])) {
398 unset($fromdb[$key]);
399 }
400 }
401 }
402
403 if (count($fromdb)) {
404 foreach ($fromdb as $entry) {
405 if (!isset($this->delete_stmt)) {
406 $delete = $this->zdb->delete(self::TABLE);
407 $delete->where([
408 'item_id' => ':item_id',
409 'field_form' => ':field_form',
410 'field_id' => ':field_id',
411 'val_index' => ':val_index'
412 ]);
413 $this->delete_stmt = $this->zdb->sql->prepareStatementForSqlObject($delete);
414 }
415 $this->delete_stmt->execute($entry);
416 //update val index
417 $field_id = $entry['field_id'];
418 if (
419 isset($this->current_values[$field_id])
420 && count($this->current_values[$field_id])
421 ) {
422 $val_index = (int)$entry['val_index'];
423 foreach ($this->current_values[$field_id] as &$current) {
424 if ((int)$current['val_index'] === $val_index + 1) {
425 $current['val_index'] = $val_index;
426 ++$val_index;
427 $current['old_val_index'] = $val_index;
428 }
429 }
430 }
431 }
432 $this->has_changed = true;
433 }
434 }
435
436 /**
437 * Is there any change in dynamic fields?
438 *
439 * @return boolean
440 */
441 public function hasChanged(): bool
442 {
443 return $this->has_changed;
444 }
445
446 /**
447 * Remove values
448 *
449 * @param ?integer $item_id Current item id to use (will be used if current item_id is 0)
450 * @param boolean $transaction True if a transaction already exists
451 *
452 * @return boolean
453 */
454 public function removeValues(int $item_id = null, bool $transaction = false): bool
455 {
456 try {
457 if ($item_id !== null && ($this->item_id === null || $this->item_id === 0)) {
458 $this->item_id = $item_id;
459 }
460 if (!$transaction) {
461 $this->zdb->connection->beginTransaction();
462 }
463
464 $delete = $this->zdb->delete(self::TABLE);
465 $delete->where(
466 array(
467 'item_id' => $this->item_id,
468 'field_form' => $this->form_name
469 )
470 );
471 $this->zdb->execute($delete);
472
473 if (!$transaction) {
474 $this->zdb->connection->commit();
475 }
476 return true;
477 } catch (Throwable $e) {
478 if (!$transaction) {
479 $this->zdb->connection->rollBack();
480 }
481 Analog::log(
482 'An error occurred removing dynamic field. Form name: ' . $this->form_name .
483 ' | Error was: ' . $e->getMessage(),
484 Analog::ERROR
485 );
486 throw $e;
487 }
488 }
489
490 /**
491 * Get current fields resultset
492 *
493 * @return ResultSet
494 */
495 protected function getCurrentFields(): ResultSet
496 {
497 $select = $this->zdb->select(self::TABLE, 'd');
498 $select->join(
499 array('t' => PREFIX_DB . DynamicField::TABLE),
500 'd.' . DynamicField::PK . '=t.' . DynamicField::PK,
501 array('field_id')
502 )->where(
503 array(
504 'item_id' => $this->item_id,
505 'd.field_form' => $this->form_name
506 )
507 );
508
509 /** only load values for accessible fields*/
510 $accessible_fields = [];
511 $access_level = $this->login->getAccessLevel();
512
513 foreach ($this->dynamic_fields as $field) {
514 $perm = $field->getPermission();
515 if (
516 ($perm == FieldsConfig::MANAGER &&
517 $access_level < Authentication::ACCESS_MANAGER) ||
518 ($perm == FieldsConfig::STAFF &&
519 $access_level < Authentication::ACCESS_STAFF) ||
520 ($perm == FieldsConfig::ADMIN &&
521 $access_level < Authentication::ACCESS_ADMIN)
522 ) {
523 continue;
524 }
525 $accessible_fields[] = $field->getId();
526 }
527
528 if (count($accessible_fields)) {
529 $select->where->in('d.' . DynamicField::PK, $accessible_fields);
530 }
531
532 $results = $this->zdb->execute($select);
533 return $results;
534 }
535 }