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\Features\Permissions
;
28 use Laminas\Db\Adapter\Adapter
;
30 use Galette\Core\Login
;
31 use Galette\Core\Authentication
;
34 * Fields config class for galette :
35 * defines fields visibility for lists and forms
36 * defines fields order and requirement flag for forms
38 * @author Johan Cwiklinski <johan@x-tnd.be>
44 public const NOBODY
= 0;
45 public const USER_WRITE
= 1;
46 public const ADMIN
= 2;
47 public const STAFF
= 3;
48 public const MANAGER
= 4;
49 public const USER_READ
= 5;
50 public const ALL
= 10;
52 public const TYPE_STR
= 0;
53 public const TYPE_HIDDEN
= 1;
54 public const TYPE_BOOL
= 2;
55 public const TYPE_INT
= 3;
56 public const TYPE_DEC
= 4;
57 public const TYPE_DATE
= 5;
58 public const TYPE_TXT
= 6;
59 public const TYPE_PASS
= 7;
60 public const TYPE_EMAIL
= 8;
61 public const TYPE_URL
= 9;
62 public const TYPE_RADIO
= 10;
63 public const TYPE_SELECT
= 11;
66 /** @var array<string, array<string, mixed>> */
67 protected array $core_db_fields = array();
68 /** @var array<string, bool> */
69 protected array $all_required = array();
70 /** @var array<string, int> */
71 protected array $all_visibles = array();
72 /** @var array<int, array<int, array<string, mixed>>> */
73 protected array $categorized_fields = array();
74 protected string $table;
75 /** @var array<string, mixed>|null */
76 protected ?
array $defaults = null;
77 /** @var array<string, mixed>|null */
78 protected ?
array $cats_defaults = null;
80 /** @var array<string> */
81 private array $staff_fields = array(
88 /** @var array<string> */
89 private array $admin_fields = array(
93 public const TABLE
= 'fields_config';
96 * Fields that are not visible in the
97 * form should not be visible here.
101 private array $non_required = array(
110 //Fields we do not want to be set as required
118 /** @var array<string> */
119 private array $non_form_elements = array(
124 /** @var array<string> */
125 private array $non_display_elements = array(
134 * Default constructor
136 * @param Db $zdb Database
137 * @param string $table the table for which to get fields configuration
138 * @param array<string, mixed> $defaults default values
139 * @param array<string, mixed> $cats_defaults default categories values
140 * @param boolean $install Are we calling from installer?
142 public function __construct(Db
$zdb, string $table, array $defaults, array $cats_defaults, bool $install = false)
145 $this->table
= $table;
146 $this->defaults
= $defaults;
147 $this->cats_defaults
= $cats_defaults;
148 //prevent check at install time...
151 $this->checkUpdate();
156 * Load current fields configuration from database.
160 public function load(): bool
163 $select = $this->zdb
->select(self
::TABLE
);
165 ->where(array('table_name' => $this->table
))
166 ->order(array(FieldsCategories
::PK
, 'position ASC'));
168 $results = $this->zdb
->execute($select);
169 $this->core_db_fields
= [];
171 foreach ($results as $k) {
172 /** @var ArrayObject<string, int|string> $k */
173 $field = $this->buildField($k);
174 $this->core_db_fields
[$k->field_id
] = $field;
179 } catch (Throwable
$e) {
181 'Fields configuration cannot be loaded!',
189 * Prepare a field (required data, automation)
191 * @param ArrayObject<string, int|string> $rset DB ResultSet row
193 * @return ArrayObject<string, int|string>
195 protected function prepareField(ArrayObject
$rset): ArrayObject
197 if ($rset->field_id
=== 'parent_id') {
198 $rset->readonly
= true;
199 $rset->required
= false;
205 * Prepare a field (required data, automation)
207 * @param ArrayObject<string, int|string> $rset DB ResultSet row
209 * @return array<string, mixed>
211 protected function buildField(ArrayObject
$rset): array
213 $rset = $this->prepareField($rset);
215 'field_id' => $rset->field_id
,
216 'label' => $this->defaults
[$rset->field_id
]['label'],
217 'category' => (int)$rset->id_field_category
,
218 'visible' => (int)$rset->visible
,
219 'required' => (bool)$rset->required
,
220 'propname' => $this->defaults
[$rset->field_id
]['propname'],
221 'position' => (int)$rset->position
,
223 'width_in_forms' => (int)$rset->width_in_forms
,
229 * Create field array configuration,
230 * Several lists of fields are kept (visible, requireds, etc), build them.
234 protected function buildLists(): void
236 $this->categorized_fields
= [];
237 $this->all_required
= [];
238 $this->all_visibles
= [];
240 foreach ($this->core_db_fields
as $field) {
241 $this->addToLists($field);
246 * Adds a field to lists
248 * @param array<string,mixed> $field Field values
252 protected function addToLists(array $field): void
254 if ($field['position'] >= 0) {
255 $this->categorized_fields
[$field['category']][] = $field;
258 //array of all required fields
259 if ($field['required']) {
260 $this->all_required
[$field['field_id']] = $field['required'];
263 //array of all fields visibility
264 $this->all_visibles
[$field['field_id']] = $field['visible'];
268 * Is a field set as required?
270 * @param string $field Field name
274 public function isRequired(string $field): bool
276 return isset($this->all_required
[$field]);
280 * Temporary set a field as not required
281 * (password for existing members for example)
283 * @param string $field Field name
287 public function setNotRequired(string $field): void
289 if (isset($this->all_required
[$field])) {
290 unset($this->all_required
[$field]);
293 foreach ($this->categorized_fields
as &$cat) {
294 foreach ($cat as &$f) {
295 if ($f['field_id'] === $field) {
296 $f['required'] = false;
304 * Checks if all fields are present in the database.
306 * For now, this function only checks if count matches.
310 private function checkUpdate(): void
312 $class = get_class($this);
315 $_all_fields = array();
316 if (count($this->core_db_fields
)) {
318 $this->core_db_fields
,
319 function ($field) use (&$_all_fields) {
320 $_all_fields[$field['field_id']] = $field;
324 //hum... no records. Let's check if any category exists
325 $select = $this->zdb
->select(FieldsCategories
::TABLE
);
326 $results = $this->zdb
->execute($select);
328 if ($results->count() == 0) {
329 //categories are missing, add them
330 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
331 $categories->installInit();
335 if (count($this->defaults
) != count($_all_fields)) {
337 'Fields configuration count for `' . $this->table
.
338 '` columns does not match records. Is : ' .
339 count($_all_fields) . ' and should be ' .
340 count($this->defaults
),
345 foreach ($this->defaults
as $k => $f) {
346 if (!isset($_all_fields[$k])) {
348 'Missing field configuration for field `' . $k . '`',
353 'table_name' => $this->table
,
354 'required' => $f['required'],
355 'visible' => $f['visible'],
356 'position' => $f['position'],
357 'category' => $f['category'],
358 'list_visible' => $f['list_visible'] ??
false,
359 'list_position' => $f['list_position'] ??
null,
360 'width_in_forms' => $f['width_in_forms'] ??
1
365 if (count($params) > 0) {
366 $this->insert($params);
370 } catch (Throwable
$e) {
372 '[' . $class . '] An error occurred while checking update for ' .
373 'fields configuration for table `' . $this->table
. '`. ' .
382 * Set default fields configuration at install time. All previous
383 * existing values will be dropped first, including fields categories.
388 public function installInit(): bool
391 $fields = array_keys($this->defaults
);
392 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
394 //first, we drop all values
395 $delete = $this->zdb
->delete(self
::TABLE
);
397 array('table_name' => $this->table
)
399 $this->zdb
->execute($delete);
400 //take care of fields categories, for db relations
401 $categories->installInit();
404 foreach ($fields as $f) {
405 //build default config for each field
408 'table_name' => $this->table
,
409 'required' => $this->defaults
[$f]['required'],
410 'visible' => $this->defaults
[$f]['visible'],
411 'position' => (int)$this->defaults
[$f]['position'],
412 'category' => $this->defaults
[$f]['category'],
413 'list_visible' => $this->defaults
[$f]['list_visible'] ??
false,
414 'list_position' => $this->defaults
[$f]['list_position'] ??
-1,
415 'width_in_forms' => $this->defaults
[$f]['width_in_forms'] ??
1
418 $this->insert($params);
421 'Default fields configuration were successfully stored.',
425 } catch (Throwable
$e) {
427 'Unable to initialize default fields configuration.' . $e->getMessage(),
436 * Get non required fields
438 * @return array<string>
440 public function getNonRequired(): array
442 return $this->non_required
;
446 * Retrieve form elements
448 * @param Login $login Login instance
449 * @param boolean $new True when adding a new member
450 * @param boolean $selfs True if we're called from self subscription page
452 * @return array<string, array<int,object>>
454 public function getFormElements(Login
$login, bool $new, bool $selfs = false): array
458 $hidden_elements = [];
461 //get columns descriptions
462 $columns = $this->zdb
->getColumns($this->table
);
464 $access_level = $login->getAccessLevel();
465 $categories = FieldsCategories
::getList($this->zdb
);
467 foreach ($categories as $c) {
468 $cpk = FieldsCategories
::PK
;
470 foreach ($this->cats_defaults
as $conf_cat) {
471 if ($conf_cat['id'] == $c->$cpk) {
472 $cat_label = $conf_cat['category'];
476 if ($cat_label === null) {
477 $cat_label = $c->category
;
479 $cat = (object)array(
480 'id' => (int)$c->$cpk,
481 'label' => $cat_label,
482 'elements' => array()
485 $elements = $this->categorized_fields
[$c->$cpk];
486 $cat->elements
= array();
488 foreach ($elements as $elt) {
490 $o->readonly
= false;
492 if ($o->field_id
== 'id_adh') {
493 // ignore access control, as member ID is always needed
494 if (!$preferences->pref_show_id ||
$new === true) {
495 $hidden_elements[] = $o;
497 $o->type
= self
::TYPE_STR
;
499 $cat->elements
[$o->field_id
] = $o;
501 } elseif ($o->field_id
== 'parent_id') {
502 $hidden_elements[] = $o;
504 // skip fields blacklisted for edition
506 in_array($o->field_id
, $this->non_form_elements
)
507 ||
$selfs && $this->isSelfExcluded($o->field_id
)
512 // skip fields according to access control
514 $o->visible
== self
::NOBODY ||
515 ($o->visible
== self
::ADMIN
&&
516 $access_level < Authentication
::ACCESS_ADMIN
) ||
517 ($o->visible
== self
::STAFF
&&
518 $access_level < Authentication
::ACCESS_STAFF
) ||
519 ($o->visible
== self
::MANAGER
&&
520 $access_level < Authentication
::ACCESS_MANAGER
)
525 if (preg_match('/date/', $o->field_id
)) {
526 $o->type
= self
::TYPE_DATE
;
527 } elseif (preg_match('/bool/', $o->field_id
)) {
528 $o->type
= self
::TYPE_BOOL
;
530 $o->field_id
== 'titre_adh'
531 ||
$o->field_id
== 'pref_lang'
532 ||
$o->field_id
== 'id_statut'
534 $o->type
= self
::TYPE_SELECT
;
535 } elseif ($o->field_id
== 'sexe_adh') {
536 $o->type
= self
::TYPE_RADIO
;
538 $o->type
= self
::TYPE_STR
;
541 //retrieve field information from DB
542 foreach ($columns as $column) {
543 if ($column->getName() === $o->field_id
) {
545 = $column->getCharacterMaximumLength();
546 $o->default = $column->getColumnDefault();
547 $o->datatype
= $column->getDataType();
552 // disabled field according to access control
554 $o->visible
== self
::USER_READ
&&
555 $access_level == Authentication
::ACCESS_USER
559 $o->disabled
= false;
562 if ($selfs === true) {
563 //email, login and password are always required for self subscription
564 $srequireds = ['email_adh', 'login_adh'];
565 if (in_array($o->field_id
, $srequireds)) {
569 $cat->elements
[$o->field_id
] = $o;
573 if (count($cat->elements
) > 0) {
574 $form_elements[] = $cat;
578 'fieldsets' => $form_elements,
579 'hiddens' => $hidden_elements
581 } catch (Throwable
$e) {
583 'An error occurred getting form elements',
591 * Retrieve display elements
593 * @param Login $login Login instance
595 * @return array<int,object>
597 public function getDisplayElements(Login
$login): array
601 $display_elements = [];
602 $access_level = $login->getAccessLevel();
603 $categories = FieldsCategories
::getList($this->zdb
);
605 foreach ($categories as $c) {
606 $cpk = FieldsCategories
::PK
;
608 foreach ($this->cats_defaults
as $conf_cat) {
609 if ($conf_cat['id'] == $c->$cpk) {
610 $cat_label = $conf_cat['category'];
614 if ($cat_label === null) {
615 $cat_label = $c->category
;
617 $cat = (object)array(
618 'id' => (int)$c->$cpk,
619 'label' => $cat_label,
620 'elements' => array()
623 $elements = $this->categorized_fields
[$c->$cpk];
624 $cat->elements
= array();
626 foreach ($elements as $elt) {
629 if ($o->field_id
== 'id_adh') {
630 // ignore access control, as member ID is always needed
631 if (!isset($preferences) ||
!$preferences->pref_show_id
) {
632 $hidden_elements[] = $o;
634 $o->type
= self
::TYPE_STR
;
635 $cat->elements
[$o->field_id
] = $o;
638 // skip fields blacklisted for display
639 if (in_array($o->field_id
, $this->non_display_elements
)) {
643 // skip fields according to access control
645 $o->visible
== self
::NOBODY ||
646 ($o->visible
== self
::ADMIN
&&
647 $access_level < Authentication
::ACCESS_ADMIN
) ||
648 ($o->visible
== self
::STAFF
&&
649 $access_level < Authentication
::ACCESS_STAFF
) ||
650 ($o->visible
== self
::MANAGER
&&
651 $access_level < Authentication
::ACCESS_MANAGER
)
656 $cat->elements
[$o->field_id
] = $o;
660 if (count($cat->elements
) > 0) {
661 $display_elements[] = $cat;
664 return $display_elements;
665 } catch (Throwable
$e) {
667 'An error occurred getting display elements',
675 * Get required fields
677 * @return array<string, bool> of all required fields. Field names = keys
679 public function getRequired(): array
681 return $this->all_required
;
687 * @return array<string,int> of all visibles fields
689 public function getVisibilities(): array
691 return $this->all_visibles
;
695 * Get visibility for specified field
697 * @param string $field The requested field
701 public function getVisibility(string $field): int
703 return $this->all_visibles
[$field];
707 * Get all fields with their categories
709 * @return array<int, array<int, array<string, mixed>>>
711 public function getCategorizedFields(): array
713 return $this->categorized_fields
;
719 * @param array<int, array<int, array<string, mixed>>> $fields categorized fields array
723 public function setFields(array $fields): bool
725 $this->categorized_fields
= $fields;
726 return $this->store();
730 * Store config in database
734 private function store(): bool
736 $class = get_class($this);
739 $this->zdb
->connection
->beginTransaction();
741 $update = $this->zdb
->update(self
::TABLE
);
744 'required' => ':required',
745 'visible' => ':visible',
746 'position' => ':position',
747 FieldsCategories
::PK
=> ':' . FieldsCategories
::PK
,
748 'width_in_forms' => ':width_in_forms'
752 'field_id' => ':field_id',
753 'table_name' => $this->table
756 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
758 foreach ($this->categorized_fields
as $cat) {
759 foreach ($cat as $pos => $field) {
760 if (in_array($field['field_id'], $this->non_required
)) {
761 $field['required'] = $this->zdb
->isPostgres() ?
'false' : 0;
764 if ($field['field_id'] === 'parent_id') {
765 $field['visible'] = 0;
769 'required' => $field['required'],
770 'visible' => $field['visible'],
772 FieldsCategories
::PK
=> $field['category'],
773 'field_id' => $field['field_id'],
774 'width_in_forms' => $field['width_in_forms']
777 $stmt->execute($params);
782 '[' . $class . '] Fields configuration stored successfully! ',
789 '[' . $class . '] Fields configuration for table %s stored ' .
795 $this->zdb
->connection
->commit();
796 return $this->load();
797 } catch (Throwable
$e) {
798 $this->zdb
->connection
->rollBack();
800 '[' . $class . '] An error occurred while storing fields ' .
801 'configuration for table `' . $this->table
. '`.' .
810 * Migrate old required fields configuration
811 * Only needeed for 0.7.4 upgrade
812 * (should have been 0.7.3 - but I missed that.)
816 public function migrateRequired(): bool
819 $select = $this->zdb
->select('required');
820 $select->from(PREFIX_DB
. 'required');
822 $old_required = $this->zdb
->execute($select);
823 } catch (\Exception
$pe) {
825 'Unable to retrieve required fields_config. Maybe ' .
826 'the table does not exists?',
833 $this->zdb
->connection
->beginTransaction();
835 $update = $this->zdb
->update(self
::TABLE
);
838 'required' => ':required'
842 'field_id' => ':field_id',
843 'table_name' => $this->table
847 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
849 foreach ($old_required as $or) {
852 'required' => ($or->required
=== false) ?
853 ($this->zdb
->isPostgres() ?
'false' : 0) : true,
854 'field_id' => $or->field_id
859 $class = get_class($this);
864 '[' . $class . '] Required fields for table %s upgraded ' .
870 $this->zdb
->db
->query(
871 'DROP TABLE ' . PREFIX_DB
. 'required',
872 Adapter
::QUERY_MODE_EXECUTE
875 $this->zdb
->connection
->commit();
877 } catch (Throwable
$e) {
878 $this->zdb
->connection
->rollBack();
880 'An error occurred migrating old required fields. | ' .
889 * Insert values in database
891 * @param array<int,mixed> $values Values to insert
895 private function insert(array $values): void
897 $insert = $this->zdb
->insert(self
::TABLE
);
900 'field_id' => ':field_id',
901 'table_name' => ':table_name',
902 'required' => ':required',
903 'visible' => ':visible',
904 FieldsCategories
::PK
=> ':category',
905 'position' => ':position',
906 'list_visible' => ':list_visible',
907 'list_position' => ':list_position',
908 'width_in_forms' => ':width_in_forms'
911 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($insert);
912 foreach ($values as $d) {
913 $required = $d['required'];
914 if ($required === false) {
915 $required = $this->zdb
->isPostgres() ?
'false' : 0;
918 $list_visible = $d['list_visible'] ??
false;
919 if ($list_visible === false) {
920 $list_visible = $this->zdb
->isPostgres() ?
'false' : 0;
925 'field_id' => $d['field_id'],
926 'table_name' => $d['table_name'],
927 'required' => $required,
928 'visible' => $d['visible'],
929 'category' => $d['category'],
930 'position' => $d['position'],
931 'list_visible' => $list_visible,
932 'list_position' => $d['list_position'] ??
-1,
933 'width_in_forms' => $d['width_in_forms'] ??
1
940 * Does field should be displayed in self subscription page
942 * @param string $name Field name
946 public function isSelfExcluded(string $name): bool
958 * Filter visible fields
960 * @param Login $login Login instance
961 * @param array<string,mixed> $fields Fields list
965 public function filterVisible(Login
$login, array &$fields): void
967 $access_level = $login->getAccessLevel();
968 $visibles = $this->getVisibilities();
970 //remove not searchable fields
971 unset($fields['mdp_adh']);
973 foreach ($fields as $k => $f) {
975 $visibles[$k] == FieldsConfig
::NOBODY ||
976 ($visibles[$k] == FieldsConfig
::ADMIN
&&
977 $access_level < Authentication
::ACCESS_ADMIN
) ||
978 ($visibles[$k] == FieldsConfig
::STAFF
&&
979 $access_level < Authentication
::ACCESS_STAFF
) ||
980 ($visibles[$k] == FieldsConfig
::MANAGER
&&
981 $access_level < Authentication
::ACCESS_MANAGER
)
989 * Get fields for massive changes
990 * @see FieldsConfig::getFormElements
992 * @param array<string,mixed> $fields Member fields
993 * @param Login $login Login instance
995 * @return array<string,mixed>
997 public function getMassiveFormElements(array $fields, Login
$login): array
999 $this->filterVisible($login, $fields);
1008 'bool_display_info',
1014 $mass_fields = array_intersect(array_keys($fields), $mass_fields);
1016 foreach ($mass_fields as $mass_field) {
1017 $this->setNotRequired($mass_field);
1019 $form_elements = $this->getFormElements($login, false);
1020 unset($form_elements['hiddens']);
1022 foreach ($form_elements['fieldsets'] as &$form_element) {
1023 $form_element->elements
= array_intersect_key($form_element->elements
, array_flip($mass_fields));
1025 return $form_elements;
1029 * Get field configuration
1031 * @param string $name Field name
1033 * @return array<string,mixed>
1035 public function getField(string $name): array
1037 if (!isset($this->core_db_fields
[$name])) {
1038 throw new \
UnexpectedValueException("$name field does not exists");
1040 return $this->core_db_fields
[$name];