4 * Copyright © 2003-2024 The Galette Team
6 * This file is part of Galette (https://galette.eu).
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.
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.
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/>.
22 namespace Galette\Entity
;
25 use Galette\DynamicFields\File
;
26 use Galette\DynamicFields\Separator
;
27 use Laminas\Db\ResultSet\ResultSet
;
30 use Laminas\Db\Adapter\Driver\StatementInterface
;
32 use Galette\Core\Login
;
33 use Galette\Core\Authentication
;
34 use Galette\DynamicFields\DynamicField
;
35 use Galette\Repository\DynamicFieldsSet
;
38 * Dynamic fields handle, aggregating field descriptors and values
40 * @author Johan Cwiklinski <johan@x-tnd.be>
43 class DynamicFieldsHandle
45 public const TABLE
= 'dynamic_fields';
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;
54 /** @var array<string> */
55 private array $errors = array();
60 private StatementInterface
$insert_stmt;
61 private StatementInterface
$update_stmt;
62 private StatementInterface
$delete_stmt;
64 private bool $has_changed = false;
69 * @param Db $zdb Database instance
70 * @param Login $login Login instance
71 * @param ?object $instance Object instance
73 public function __construct(Db
$zdb, Login
$login, object $instance = null)
76 $this->login
= $login;
77 if ($instance !== null) {
78 $this->load($instance);
83 * Load dynamic fields values for specified object
85 * @param object $object Object instance
89 public function load(object $object): bool
91 $this->form_name
= $object->getFormName();
94 $this->item_id
= $object->id
;
95 $fields = new DynamicFieldsSet($this->zdb
, $this->login
);
96 $this->dynamic_fields
= $fields->getList($this->form_name
);
98 $results = $this->getCurrentFields();
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;
112 //something went wrong here :(
114 'Dynamic choice value "' . $f->field_val
. '" does not exists!',
117 $f->text_val
= $f->field_val
;
120 $f->text_val
= $choices[$f->field_val
];
123 $this->current_values
[$f->{DynamicField
::PK
}][] = array_filter(
125 static function ($k) {
126 return $k != DynamicField
::PK
;
132 'Dynamic values found for ' . get_class($object) . ' #' . $this->item_id
.
133 '; but no dynamic field configured!',
142 } catch (Throwable
$e) {
144 __METHOD__
. ' | ' . $e->getMessage(),
154 * @return array<string>
156 public function getErrors(): array
158 return $this->errors
;
164 * @return array<int, DynamicField>
166 public function getFields(): array
168 return $this->dynamic_fields
;
172 * Get fields for search pages
174 * @return array<int, DynamicField>
176 public function getSearchFields(): array
178 $dynamics = $this->dynamic_fields
;
180 foreach ($dynamics as $key => $field) {
181 if ($field instanceof Separator ||
$field instanceof File
) {
182 unset($dynamics[$key]);
192 * @param integer $field Field ID
194 * @return array<int, array<string, mixed>>
196 public function getValues(int $field): array
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(),
207 return $this->current_values
[$field];
213 * @param ?integer $item Item ID
214 * @param integer $field Field ID
215 * @param integer $index Value index
216 * @param string|int $value Value
220 public function setValue(?
int $item, int $field, int $index, string|
int $value): void
225 'field_form' => $this->dynamic_fields
[$field]->getForm(),
226 'val_index' => $index,
227 'field_val' => $value,
230 if (!isset($this->current_values
[$field][$idx])) {
231 $input['is_new'] = true;
234 $this->current_values
[$field][$idx] = $input;
240 * @param integer $field Field ID
241 * @param integer $index Value index
245 public function unsetValue(int $field, int $index): void
248 if (isset($this->current_values
[$field][$idx])) {
249 unset($this->current_values
[$field][$idx]);
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
261 public function storeValues(int $item_id = null, bool $transaction = false): bool
264 if ($item_id !== null && ($this->item_id
=== null ||
$this->item_id
=== 0)) {
265 $this->item_id
= $item_id;
268 $this->zdb
->connection
->beginTransaction();
271 $this->handleRemovals();
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
;
280 if (isset($value['is_new'])) {
281 unset($value['is_new']);
282 $this->getInsertStatement()->execute($value);
283 $this->has_changed
= true;
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
293 $this->getUpdateStatement()->execute($params);
294 $this->has_changed
= true;
300 $this->zdb
->connection
->commit();
303 } catch (Throwable
$e) {
305 $this->zdb
->connection
->rollBack();
308 'An error occurred storing dynamic field. Form name: ' . $this->form_name
.
309 ' | Error was: ' . $e->getMessage(),
322 * Get (and prepare if not done yet) insert statement
324 * @return StatementInterface
326 private function getInsertStatement(): StatementInterface
328 if (!isset($this->insert_stmt
)) {
329 $insert = $this->zdb
->insert(self
::TABLE
);
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'
337 $this->insert_stmt
= $this->zdb
->sql
->prepareStatementForSqlObject($insert);
339 return $this->insert_stmt
;
343 * Get (and prepare if not done yet) update statement
345 * @return StatementInterface
347 private function getUpdateStatement(): StatementInterface
349 if (!isset($this->update_stmt
)) {
350 $update = $this->zdb
->update(self
::TABLE
);
352 'field_val' => ':field_val',
353 'val_index' => ':val_index'
355 'item_id' => ':item_id',
356 'field_id' => ':field_id',
357 'field_form' => ':field_form',
358 'val_index' => ':old_val_index'
360 $this->update_stmt
= $this->zdb
->sql
->prepareStatementForSqlObject($update);
362 return $this->update_stmt
;
366 * Handle values that have been removed
370 private function handleRemovals(): void
372 $fields = new DynamicFieldsSet($this->zdb
, $this->login
);
373 $this->dynamic_fields
= $fields->getList($this->form_name
);
375 $results = $this->getCurrentFields();
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
389 if (!count($fromdb)) {
390 //no entry in database, nothing to do.
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]);
403 if (count($fromdb)) {
404 foreach ($fromdb as $entry) {
405 if (!isset($this->delete_stmt
)) {
406 $delete = $this->zdb
->delete(self
::TABLE
);
408 'item_id' => ':item_id',
409 'field_form' => ':field_form',
410 'field_id' => ':field_id',
411 'val_index' => ':val_index'
413 $this->delete_stmt
= $this->zdb
->sql
->prepareStatementForSqlObject($delete);
415 $this->delete_stmt
->execute($entry);
417 $field_id = $entry['field_id'];
419 isset($this->current_values
[$field_id])
420 && count($this->current_values
[$field_id])
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;
427 $current['old_val_index'] = $val_index;
432 $this->has_changed
= true;
437 * Is there any change in dynamic fields?
441 public function hasChanged(): bool
443 return $this->has_changed
;
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
454 public function removeValues(int $item_id = null, bool $transaction = false): bool
457 if ($item_id !== null && ($this->item_id
=== null ||
$this->item_id
=== 0)) {
458 $this->item_id
= $item_id;
461 $this->zdb
->connection
->beginTransaction();
464 $delete = $this->zdb
->delete(self
::TABLE
);
467 'item_id' => $this->item_id
,
468 'field_form' => $this->form_name
471 $this->zdb
->execute($delete);
474 $this->zdb
->connection
->commit();
477 } catch (Throwable
$e) {
479 $this->zdb
->connection
->rollBack();
482 'An error occurred removing dynamic field. Form name: ' . $this->form_name
.
483 ' | Error was: ' . $e->getMessage(),
491 * Get current fields resultset
495 protected function getCurrentFields(): ResultSet
497 $select = $this->zdb
->select(self
::TABLE
, 'd');
499 array('t' => PREFIX_DB
. DynamicField
::TABLE
),
500 'd.' . DynamicField
::PK
. '=t.' . DynamicField
::PK
,
504 'item_id' => $this->item_id
,
505 'd.field_form' => $this->form_name
509 /** only load values for accessible fields*/
510 $accessible_fields = [];
511 $access_level = $this->login
->getAccessLevel();
513 foreach ($this->dynamic_fields
as $field) {
514 $perm = $field->getPermission();
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
)
525 $accessible_fields[] = $field->getId();
528 if (count($accessible_fields)) {
529 $select->where
->in('d.' . DynamicField
::PK
, $accessible_fields);
532 $results = $this->zdb
->execute($select);