3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Fields config handling
10 * Copyright © 2009-2020 The Galette Team
12 * This file is part of Galette (http://galette.tuxfamily.org).
14 * Galette is free software: you can redistribute it and/or modify
15 * it under the terms of the GNU General Public License as published by
16 * the Free Software Foundation, either version 3 of the License, or
17 * (at your option) any later version.
19 * Galette is distributed in the hope that it will be useful,
20 * but WITHOUT ANY WARRANTY; without even the implied warranty of
21 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 * GNU General Public License for more details.
24 * You should have received a copy of the GNU General Public License
25 * along with Galette. If not, see <http://www.gnu.org/licenses/>.
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2009-2020 The Galette Team
32 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
33 * @link http://galette.tuxfamily.org
34 * @since Available since 0.7dev - 2009-03-26
37 namespace Galette\Entity
;
41 use Laminas\Db\Adapter\Adapter
;
43 use Galette\Core\Login
;
44 use Galette\Core\Authentication
;
47 * Fields config class for galette :
48 * defines fields visibility for lists and forms
49 * defines fields order and requirement flag for forms
54 * @author Johan Cwiklinski <johan@x-tnd.be>
55 * @copyright 2009-2020 The Galette Team
56 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
57 * @link http://galette.tuxfamily.org
58 * @since Available since 0.7dev - 2009-03-26
70 const TYPE_HIDDEN
= 1;
79 const TYPE_RADIO
= 10;
80 const TYPE_SELECT
= 11;
83 protected $core_db_fields = array();
84 protected $all_required = array();
85 protected $all_visibles = array();
86 protected $categorized_fields = array();
88 protected $defaults = null;
89 protected $cats_defaults = null;
91 private $staff_fields = array(
98 private $admin_fields = array(
102 const TABLE
= 'fields_config';
105 * Fields that are not visible in the
106 * form should not be visible here.
108 private $non_required = array(
117 //Fields we do not want to be set as required
125 private $non_form_elements = array(
130 private $non_display_elements = array(
140 * Default constructor
142 * @param Db $zdb Database
143 * @param string $table the table for which to get fields configuration
144 * @param array $defaults default values
145 * @param array $cats_defaults default categories values
146 * @param boolean $install Are we calling from installer?
148 public function __construct(Db
$zdb, $table, $defaults, $cats_defaults, $install = false)
151 $this->table
= $table;
152 $this->defaults
= $defaults;
153 $this->cats_defaults
= $cats_defaults;
154 //prevent check at install time...
157 $this->checkUpdate();
162 * Load current fields configuration from database.
166 public function load()
169 $select = $this->zdb
->select(self
::TABLE
);
171 ->where(array('table_name' => $this->table
))
172 ->order(array(FieldsCategories
::PK
, 'position ASC'));
174 $results = $this->zdb
->execute($select);
175 $this->core_db_fields
= [];
177 foreach ($results as $k) {
178 $field = $this->buildField($k);
179 $this->core_db_fields
[$k->field_id
] = $field;
184 } catch (\Exception
$e) {
186 'Fields configuration cannot be loaded!',
194 * Prepare a field (required data, automation)
196 * @param ArrayObject $rset DB ResultSet row
198 * @return ArrayObject
200 protected function prepareField(ArrayObject
$rset): ArrayObject
202 if ($rset->field_id
=== 'parent_id') {
203 $rset->readonly
= true;
204 $rset->required
= false;
210 * Prepare a field (required data, automation)
212 * @param ArrayObject $rset DB ResultSet row
216 protected function buildField(ArrayObject
$rset): array
218 $rset = $this->prepareField($rset);
220 'field_id' => $rset->field_id
,
221 'label' => $this->defaults
[$rset->field_id
]['label'],
222 'category' => (int)$rset->id_field_category
,
223 'visible' => (int)$rset->visible
,
224 'required' => (bool)$rset->required
,
225 'propname' => $this->defaults
[$rset->field_id
]['propname'],
226 'position' => (int)$rset->position
,
233 * Create field array configuration,
234 * Several lists of fields are kept (visible, requireds, etc), build them.
238 protected function buildLists()
241 $this->categorized_fields
= [];
242 $this->all_required
= [];
243 $this->all_visibles
= [];
245 foreach ($this->core_db_fields
as $field) {
246 $this->addToLists($field);
251 * Adds a field to lists
253 * @param array $field Field values
257 protected function addToLists(array $field)
259 if ($field['position'] >= 0) {
260 $this->categorized_fields
[$field['category']][] = $field;
263 //array of all required fields
264 if ($field['required']) {
265 $this->all_required
[$field['field_id']] = $field['required'];
268 //array of all fields visibility
269 $this->all_visibles
[$field['field_id']] = $field['visible'];
273 * Is a field set as required?
275 * @param string $field Field name
279 public function isRequired($field)
281 return isset($this->all_required
[$field]);
285 * Temporary set a field as not required
286 * (password for existing members for example)
288 * @param string $field Field name
292 public function setNotRequired($field)
294 if (isset($this->all_required
[$field])) {
295 unset($this->all_required
[$field]);
298 foreach ($this->categorized_fields
as &$cat) {
299 foreach ($cat as &$f) {
300 if ($f['field_id'] === $field) {
301 $f['required'] = false;
309 * Checks if all fields are present in the database.
311 * For now, this function only checks if count matches.
315 private function checkUpdate()
317 $class = get_class($this);
320 $_all_fields = array();
321 if (count($this->core_db_fields
)) {
323 $this->core_db_fields
,
324 function ($field) use (&$_all_fields) {
325 $_all_fields[$field['field_id']] = $field;
329 //hum... no records. Let's check if any category exists
330 $select = $this->zdb
->select(FieldsCategories
::TABLE
);
331 $results = $this->zdb
->execute($select);
333 if ($results->count() == 0) {
334 //categories are missing, add them
335 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
336 $categories->installInit();
340 if (count($this->defaults
) != count($_all_fields)) {
342 'Fields configuration count for `' . $this->table
.
343 '` columns does not match records. Is : ' .
344 count($_all_fields) . ' and should be ' .
345 count($this->defaults
),
350 foreach ($this->defaults
as $k => $f) {
351 if (!isset($_all_fields[$k])) {
353 'Missing field configuration for field `' . $k . '`',
358 'table_name' => $this->table
,
359 'required' => $f['required'],
360 'visible' => $f['visible'],
361 'position' => $f['position'],
362 'category' => $f['category'],
363 'list_visible' => $f['list_visible'] ??
false,
364 'list_position' => $f['list_position'] ??
null
369 if (count($params) > 0) {
370 $this->insert($params);
374 } catch (\Exception
$e) {
376 '[' . $class . '] An error occurred while checking update for ' .
377 'fields configuration for table `' . $this->table
. '`. ' .
386 * Set default fields configuration at install time. All previous
387 * existing values will be dropped first, including fields categories.
389 * @return boolean|\Exception
391 public function installInit()
394 $fields = array_keys($this->defaults
);
395 $categories = new FieldsCategories($this->zdb
, $this->cats_defaults
);
397 //first, we drop all values
398 $delete = $this->zdb
->delete(self
::TABLE
);
400 array('table_name' => $this->table
)
402 $this->zdb
->execute($delete);
403 //take care of fields categories, for db relations
404 $categories->installInit($this->zdb
);
406 $fields = array_keys($this->defaults
);
407 foreach ($fields as $f) {
408 //build default config for each field
411 'table_name' => $this->table
,
412 'required' => $this->defaults
[$f]['required'],
413 'visible' => $this->defaults
[$f]['visible'],
414 'position' => (int)$this->defaults
[$f]['position'],
415 'category' => $this->defaults
[$f]['category'],
416 'list_visible' => $this->defaults
[$f]['list_visible'] ??
false,
417 'list_position' => $this->defaults
[$f]['list_position'] ??
-1
420 $this->insert($params);
423 'Default fields configuration were successfully stored.',
427 } catch (\Exception
$e) {
429 'Unable to initialize default fields configuration.' . $e->getMessage(),
433 /*$messages = array();
435 $messages[] = $e->getMessage();
436 } while ($e = $e->getPrevious());
439 'Unable to initialize default fields configuration.' .
440 implode("\n", $messages),
448 * Get non required fields
452 public function getNonRequired()
454 return $this->non_required
;
458 * Retrieve form elements
460 * @param Login $login Login instance
461 * @param boolean $new True when adding a new member
462 * @param boolean $selfs True if we're called from self subscription page
466 public function getFormElements(Login
$login, $new, $selfs = false)
470 $hidden_elements = [];
473 //get columns descriptions
474 $columns = $this->zdb
->getColumns($this->table
);
476 $access_level = $login->getAccessLevel();
477 $categories = FieldsCategories
::getList($this->zdb
);
479 foreach ($categories as $c) {
480 $cpk = FieldsCategories
::PK
;
482 foreach ($this->cats_defaults
as $conf_cat) {
483 if ($conf_cat['id'] == $c->$cpk) {
484 $cat_label = $conf_cat['category'];
488 if ($cat_label === null) {
489 $cat_label = $c->category
;
491 $cat = (object)array(
492 'id' => (int)$c->$cpk,
493 'label' => $cat_label,
494 'elements' => array()
497 $elements = $this->categorized_fields
[$c->$cpk];
498 $cat->elements
= array();
500 foreach ($elements as $elt) {
502 $o->readonly
= false;
504 if ($o->field_id
== 'id_adh') {
505 // ignore access control, as member ID is always needed
506 if (!$preferences->pref_show_id ||
$new === true) {
507 $hidden_elements[] = $o;
509 $o->type
= self
::TYPE_STR
;
511 $cat->elements
[$o->field_id
] = $o;
513 } elseif ($o->field_id
== 'parent_id') {
514 $hidden_elements[] = $o;
516 // skip fields blacklisted for edition
518 in_array($o->field_id
, $this->non_form_elements
)
519 ||
$selfs && $this->isSelfExcluded($o->field_id
)
524 // skip fields according to access control
526 $o->visible
== self
::NOBODY ||
527 ($o->visible
== self
::ADMIN
&&
528 $access_level < Authentication
::ACCESS_ADMIN
) ||
529 ($o->visible
== self
::STAFF
&&
530 $access_level < Authentication
::ACCESS_STAFF
) ||
531 ($o->visible
== self
::MANAGER
&&
532 $access_level < Authentication
::ACCESS_MANAGER
)
537 if (preg_match('/date/', $o->field_id
)) {
538 $o->type
= self
::TYPE_DATE
;
539 } elseif (preg_match('/bool/', $o->field_id
)) {
540 $o->type
= self
::TYPE_BOOL
;
542 $o->field_id
== 'titre_adh'
543 ||
$o->field_id
== 'pref_lang'
544 ||
$o->field_id
== 'id_statut'
546 $o->type
= self
::TYPE_SELECT
;
547 } elseif ($o->field_id
== 'sexe_adh') {
548 $o->type
= self
::TYPE_RADIO
;
550 $o->type
= self
::TYPE_STR
;
553 //retrieve field information from DB
554 foreach ($columns as $column) {
555 if ($column->getName() === $o->field_id
) {
557 = $column->getCharacterMaximumLength();
558 $o->default = $column->getColumnDefault();
559 $o->datatype
= $column->getDataType();
564 // disabled field according to access control
566 $o->visible
== self
::USER_READ
&&
567 $access_level == Authentication
::ACCESS_USER
571 $o->disabled
= false;
574 if ($selfs === true) {
575 //email, login and password are always required for self subscription
576 $srequireds = ['email_adh', 'mdp_adh', 'login_adh'];
577 if (in_array($o->field_id
, $srequireds)) {
581 $cat->elements
[$o->field_id
] = $o;
585 if (count($cat->elements
) > 0) {
586 $form_elements[] = $cat;
590 'fieldsets' => $form_elements,
591 'hiddens' => $hidden_elements
593 } catch (\Exception
$e) {
595 'An error occurred getting form elements',
603 * Retrieve display elements
605 * @param Login $login Login instance
609 public function getDisplayElements(Login
$login)
613 $display_elements = [];
614 $access_level = $login->getAccessLevel();
615 $categories = FieldsCategories
::getList($this->zdb
);
617 foreach ($categories as $c) {
618 $cpk = FieldsCategories
::PK
;
620 foreach ($this->cats_defaults
as $conf_cat) {
621 if ($conf_cat['id'] == $c->$cpk) {
622 $cat_label = $conf_cat['category'];
626 if ($cat_label === null) {
627 $cat_label = $c->category
;
629 $cat = (object)array(
630 'id' => (int)$c->$cpk,
631 'label' => $cat_label,
632 'elements' => array()
635 $elements = $this->categorized_fields
[$c->$cpk];
636 $cat->elements
= array();
638 foreach ($elements as $elt) {
641 if ($o->field_id
== 'id_adh') {
642 // ignore access control, as member ID is always needed
643 if (!isset($preferences) ||
!$preferences->pref_show_id
) {
644 $hidden_elements[] = $o;
646 $o->type
= self
::TYPE_STR
;
647 $cat->elements
[$o->field_id
] = $o;
650 // skip fields blacklisted for display
651 if (in_array($o->field_id
, $this->non_display_elements
)) {
655 // skip fields according to access control
657 $o->visible
== self
::NOBODY ||
658 ($o->visible
== self
::ADMIN
&&
659 $access_level < Authentication
::ACCESS_ADMIN
) ||
660 ($o->visible
== self
::STAFF
&&
661 $access_level < Authentication
::ACCESS_STAFF
) ||
662 ($o->visible
== self
::MANAGER
&&
663 $access_level < Authentication
::ACCESS_MANAGER
)
668 $cat->elements
[$o->field_id
] = $o;
672 if (count($cat->elements
) > 0) {
673 $display_elements[] = $cat;
676 return $display_elements;
677 } catch (\Exception
$e) {
679 'An error occurred getting display elements',
687 * Get required fields
689 * @return array of all required fields. Field names = keys
691 public function getRequired()
693 return $this->all_required
;
699 * @return array of all visibles fields
701 public function getVisibilities()
703 return $this->all_visibles
;
707 * Get visibility for specified field
709 * @param string $field The requested field
713 public function getVisibility($field)
715 return $this->all_visibles
[$field];
719 * Get all fields with their categories
723 public function getCategorizedFields()
725 return $this->categorized_fields
;
731 * @param array $fields categorized fields array
735 public function setFields($fields)
737 $this->categorized_fields
= $fields;
738 return $this->store();
742 * Store config in database
746 private function store()
748 $class = get_class($this);
751 $this->zdb
->connection
->beginTransaction();
753 $update = $this->zdb
->update(self
::TABLE
);
756 'required' => ':required',
757 'visible' => ':visible',
758 'position' => ':position',
759 FieldsCategories
::PK
=> ':category'
763 'field_id' => ':field_id',
764 'table_name' => $this->table
767 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
770 foreach ($this->categorized_fields
as $cat) {
771 foreach ($cat as $pos => $field) {
772 if (in_array($field['field_id'], $this->non_required
)) {
773 $field['required'] = $this->zdb
->isPostgres() ?
'false' : 0;
776 if ($field['field_id'] === 'parent_id') {
777 $field['visible'] = 0;
781 'required' => $field['required'],
782 'visible' => $field['visible'],
784 FieldsCategories
::PK
=> $field['category'],
785 'where1' => $field['field_id']
788 $stmt->execute($params);
793 '[' . $class . '] Fields configuration stored successfully! ',
800 '[' . $class . '] Fields configuration for table %s stored ' .
806 $this->zdb
->connection
->commit();
807 return $this->load();
808 } catch (\Exception
$e) {
809 $this->zdb
->connection
->rollBack();
811 '[' . $class . '] An error occurred while storing fields ' .
812 'configuration for table `' . $this->table
. '`.' .
817 $e->getTraceAsString(),
825 * Migrate old required fields configuration
826 * Only needeed for 0.7.4 upgrade
827 * (should have been 0.7.3 - but I missed that.)
831 public function migrateRequired()
833 $old_required = null;
836 $select = $this->zdb
->select('required');
837 $select->from(PREFIX_DB
. 'required');
839 $old_required = $this->zdb
->execute($select);
840 } catch (\Exception
$pe) {
842 'Unable to retrieve required fields_config. Maybe ' .
843 'the table does not exists?',
850 $this->zdb
->connection
->beginTransaction();
852 $update = $this->zdb
->update(self
::TABLE
);
855 'required' => ':required'
859 'field_id' => ':field_id',
860 'table_name' => $this->table
864 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($update);
866 foreach ($old_required as $or) {
867 /** Why where parameter is named where1 ?? */
870 'required' => ($or->required
=== false) ?
871 ($this->zdb
->isPostgres() ?
'false' : 0) : true,
872 'where1' => $or->field_id
877 $class = get_class($this);
882 '[' . $class . '] Required fields for table %s upgraded ' .
888 $this->zdb
->db
->query(
889 'DROP TABLE ' . PREFIX_DB
. 'required',
890 Adapter
::QUERY_MODE_EXECUTE
893 $this->zdb
->connection
->commit();
895 } catch (\Exception
$e) {
896 $this->zdb
->connection
->rollBack();
898 'An error occurred migrating old required fields. | ' .
907 * Insert values in database
909 * @param array $values Values to insert
913 private function insert($values)
915 $insert = $this->zdb
->insert(self
::TABLE
);
918 'field_id' => ':field_id',
919 'table_name' => ':table_name',
920 'required' => ':required',
921 'visible' => ':visible',
922 FieldsCategories
::PK
=> ':category',
923 'position' => ':position',
924 'list_visible' => ':list_visible',
925 'list_position' => ':list_position'
928 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($insert);
929 foreach ($values as $d) {
930 $required = $d['required'];
931 if ($required === false) {
932 $required = $this->zdb
->isPostgres() ?
'false' : 0;
935 $list_visible = $f['list_visible'] ??
false;
936 if ($list_visible === false) {
937 $list_visible = $this->zdb
->isPostgres() ?
'false' : 0;
942 'field_id' => $d['field_id'],
943 'table_name' => $d['table_name'],
944 'required' => $required,
945 'visible' => $d['visible'],
946 FieldsCategories
::PK
=> $d['category'],
947 'position' => $d['position'],
948 'list_visible' => $list_visible,
949 'list_position' => $d['list_position'] ??
-1
956 * Does field should be displayed in self subscription page
958 * @param string $name Field name
962 public function isSelfExcluded($name)
974 * Get fields for massive changes
975 * @see FieldsConfig::getFormElements
977 * @param array $fields Member fields
978 * @param Login $login Login instance
982 public function getMassiveFormElements(array $fields, Login
$login)
984 $visibles = $this->getVisibilities();
985 $access_level = $login->getAccessLevel();
987 //remove not searchable fields
988 unset($fields['mdp_adh']);
990 foreach ($fields as $k => $f) {
992 $visibles[$k] == FieldsConfig
::NOBODY ||
993 ($visibles[$k] == FieldsConfig
::ADMIN
&&
994 $access_level < Authentication
::ACCESS_ADMIN
) ||
995 ($visibles[$k] == FieldsConfig
::STAFF
&&
996 $access_level < Authentication
::ACCESS_STAFF
) ||
997 ($visibles[$k] == FieldsConfig
::MANAGER
&&
998 $access_level < Authentication
::ACCESS_MANAGER
)
1011 'bool_display_info',
1017 $mass_fields = array_intersect(array_keys($fields), $mass_fields);
1019 foreach ($mass_fields as $mass_field) {
1020 $this->setNotRequired($mass_field);
1022 $form_elements = $this->getFormElements($login, false);
1023 unset($form_elements['hiddens']);
1025 foreach ($form_elements['fieldsets'] as &$form_element) {
1026 $form_element->elements
= array_intersect_key($form_element->elements
, array_flip($mass_fields));
1028 return $form_elements;
1032 * Get field configuration
1034 * @param string $name Field name
1038 public function getField($name): array
1040 if (!isset($this->core_db_fields
[$name])) {
1041 throw new \
UnexpectedValueException("$name fied does not exists");
1043 return $this->core_db_fields
[$name];