3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Abstract dynamic field
10 * Copyright © 2012-2023 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/>.
27 * @category DynamicFields
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2012-2023 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.7.1dev - 2012-07-28
37 namespace Galette\DynamicFields
;
43 use Galette\Entity\DynamicFieldsHandle
;
44 use Galette\Features\Translatable
;
45 use Galette\Features\I18n
;
46 use Laminas\Db\Sql\Expression
;
47 use Laminas\Db\Sql\Predicate\Expression
as PredicateExpression
;
50 * Abstract dynamic field
53 * @category DynamicFields
56 * @author Johan Cwiklinski <johan@x-tnd.be>
57 * @copyright 2012-2023 The Galette Team
58 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
59 * @link http://galette.tuxfamily.org
62 abstract class DynamicField
67 public const TABLE
= 'field_types';
68 public const PK
= 'field_id';
70 /** Separator field */
71 public const SEPARATOR
= 0;
72 /** Simple text field */
73 public const TEXT
= 1;
75 public const LINE
= 2;
76 /** Choice field (listbox) */
77 public const CHOICE
= 3;
79 public const DATE
= 4;
80 /** Boolean field (checkbox) */
81 public const BOOLEAN
= 5;
82 /** File field (upload) */
83 public const FILE
= 6;
85 public const MOVE_UP
= 'up';
86 public const MOVE_DOWN
= 'down';
88 public const PERM_USER_WRITE
= 0;
89 public const PERM_ADMIN
= 1;
90 public const PERM_STAFF
= 2;
91 public const PERM_MANAGER
= 3;
92 public const PERM_USER_READ
= 4;
94 public const DEFAULT_MAX_FILE_SIZE
= 1024;
95 public const VALUES_FIELD_LENGTH
= 100;
97 protected $has_data = false;
98 protected $has_width = false;
99 protected $has_height = false;
100 protected $has_size = false;
101 protected $multi_valued = false;
102 protected $fixed_values = false;
103 protected $has_permissions = true;
116 protected $information;
120 protected $errors = [];
125 * Default constructor
127 * @param Db $zdb Database instance
128 * @param mixed $args Arguments
130 public function __construct(Db
$zdb, $args = null)
136 } elseif (is_object($args)) {
137 $this->loadFromRs($args);
142 * Load field from its id
144 * @param Db $zdb Database instance
145 * @param int $id Field id
147 * @return DynamicField|false
149 public static function loadFieldType(Db
$zdb, int $id)
152 $select = $zdb->select(self
::TABLE
);
153 $select->where(['field_id' => $id]);
155 $results = $zdb->execute($select);
156 if ($results->count() > 0) {
157 /** @var ArrayObject $result */
158 $result = $results->current();
159 $field_type = $result->field_type
;
160 $field_type = self
::getFieldType($zdb, $field_type);
161 $field_type->loadFromRs($result);
164 } catch (Throwable
$e) {
166 __METHOD__
. ' | Unable to retrieve field `' . $id .
167 '` information | ' . $e->getMessage(),
176 * Get correct field type instance
178 * @param Db $zdb Database instance
179 * @param int $t Field type
180 * @param int|null $id Optional dynamic field id (to load data)
182 * @return DynamicField
184 public static function getFieldType(Db
$zdb, int $t, int $id = null)
188 case self
::SEPARATOR
:
189 $df = new Separator($zdb, $id);
192 $df = new Text($zdb, $id);
195 $df = new Line($zdb, $id);
198 $df = new Choice($zdb, $id);
201 $df = new Date($zdb, $id);
204 $df = new Boolean($zdb, $id);
207 $df = new File($zdb, $id);
210 throw new \
Exception('Unknown field type ' . $t . '!');
218 * @param integer $id Id
222 public function load(int $id): void
225 $select = $this->zdb
->select(self
::TABLE
);
226 $select->where([self
::PK
=> $id]);
228 $results = $this->zdb
->execute($select);
229 if ($results->count() > 0) {
230 /** @var ArrayObject $result */
231 $result = $results->current();
232 $this->loadFromRs($result);
234 } catch (Throwable
$e) {
236 'Unable to retrieve field type for field ' . $id . ' | ' .
244 * Load field type from a db ResultSet
246 * @param ArrayObject $rs ResultSet
247 * @param bool $values Whether to load values. Defaults to true
251 public function loadFromRs(ArrayObject
$rs, bool $values = true): void
253 $this->id
= (int)$rs->field_id
;
254 $this->name
= $rs->field_name
;
255 $this->index
= (int)$rs->field_index
;
256 $this->perm
= (int)$rs->field_perm
;
257 $this->required
= $rs->field_required
== 1;
258 $this->width
= $rs->field_width
;
259 $this->height
= $rs->field_height
;
260 $this->repeat
= (int)$rs->field_repeat
;
261 $this->size
= $rs->field_size
;
262 $this->form
= $rs->field_form
;
263 $this->information
= $rs->field_information
;
264 if ($values && $this->hasFixedValues()) {
265 $this->loadFixedValues();
270 * Retrieve fixed values table name
272 * @param integer $id Field ID
273 * @param bool $prefixed Whether table name should be prefixed
277 public static function getFixedValuesTableName(int $id, bool $prefixed = false): string
279 $name = 'field_contents_' . $id;
280 if ($prefixed === true) {
281 $name = PREFIX_DB
. $name;
287 * Returns an array of fixed valued for a field of type 'choice'.
291 private function loadFixedValues()
294 $val_select = $this->zdb
->select(
295 self
::getFixedValuesTableName($this->id
)
298 $val_select->columns(
304 $results = $this->zdb
->execute($val_select);
305 $this->values
= array();
306 if ($results->count() > 0) {
307 foreach ($results as $val) {
308 $this->values
[] = $val->val
;
311 } catch (Throwable
$e) {
313 __METHOD__
. ' | ' . $e->getMessage(),
324 abstract public function getType(): int;
327 * Get field type name
331 public function getTypeName(): string
333 $types = $this->getFieldsTypesNames();
334 if (isset($types[$this->getType()])) {
335 return $types[$this->getType()];
337 throw new \
RuntimeException(
338 'Unknown type ' . $this->getType()
344 * Does the field handle data?
348 public function hasData(): bool
350 return (bool)$this->has_data
;
354 * Does the field has width?
358 public function hasWidth(): bool
360 return (bool)$this->has_width
;
364 * Does the field has height?
368 public function hasHeight(): bool
370 return (bool)$this->has_height
;
374 * Does the field has a size?
378 public function hasSize(): bool
380 return (bool)$this->has_size
;
384 * Is the field multivalued?
388 public function isMultiValued(): bool
390 return (bool)$this->multi_valued
;
394 * Does the field has fixed values?
398 public function hasFixedValues(): bool
400 return (bool)$this->fixed_values
;
404 * Does the field require permissions?
408 public function hasPermissions(): bool
410 return (bool)$this->has_permissions
;
416 * @return integer|null
418 public function getId(): ?
int
424 * Get field Permissions
426 * @return integer|null
428 public function getPerm(): ?
int
438 public function isRequired(): bool
440 return (bool)$this->required
;
446 * @return integer|null
448 public function getWidth(): ?
int
456 * @return integer|null
458 public function getHeight(): ?
int
460 return $this->height
;
464 * Is current field repeatable?
468 public function isRepeatable(): bool
470 return $this->repeat
!= null && trim($this->repeat
) != '' && (int)$this->repeat
>= 0;
474 * Get fields repetitions
476 * @return integer|null
478 public function getRepeat(): ?
int
480 return $this->repeat
;
486 * @return integer|null
488 public function getSize(): ?
int
496 * @return integer|null
498 public function getIndex(): ?
int
504 * Get field information
508 public function getInformation(): string
510 return $this->information ??
'';
514 * Retrieve permissions names for display
518 public static function getPermsNames(): array
521 self
::PERM_USER_WRITE
=> _T("User, read/write"),
522 self
::PERM_STAFF
=> _T("Staff member"),
523 self
::PERM_ADMIN
=> _T("Administrator"),
524 self
::PERM_MANAGER
=> _T("Group manager"),
525 self
::PERM_USER_READ
=> _T("User, read only")
530 * Retrieve forms names
534 public static function getFormsNames(): array
537 'adh' => _T("Members"),
538 'contrib' => _T("Contributions"),
539 'trans' => _T("Transactions")
546 * @param string $form_name Form name
550 public static function getFormTitle(string $form_name): string
552 $names = self
::getFormsNames();
553 return $names[$form_name];
557 * Get permission name
561 public function getPermName(): string
563 $perms = self
::getPermsNames();
564 return $perms[$this->getPerm()];
572 public function getForm(): string
580 * @param bool $imploded Whether to implode values
582 * @return array|string|false
584 public function getValues(bool $imploded = false)
586 if (!is_array($this->values
)) {
589 if ($imploded === true) {
590 return implode("\n", $this->values
);
592 return $this->values
;
597 * Check posted values validity
599 * @param array $values All values to check, basically the $_POST array
600 * after sending the form
604 public function check(array $values)
607 $this->warnings
= [];
610 (!isset($values['field_name']) ||
$values['field_name'] == '')
611 && get_class($this) != '\Galette\DynamicField\Separator'
613 $this->errors
[] = _T('Missing required field name!');
615 if ($this->old_name
=== null && $this->name
!== null && $this->name
!= $values['field_name']) {
616 $this->old_name
= $this->name
;
618 $this->name
= $values['field_name'];
621 if (!isset($values['field_perm']) ||
$values['field_perm'] === '') {
622 $this->errors
[] = _T('Missing required field permissions!');
624 if (in_array($values['field_perm'], array_keys(self
::getPermsNames()))) {
625 $this->perm
= $values['field_perm'];
627 $this->errors
[] = _T('Unknown permission!');
631 if ($this->id
=== null) {
632 if (!isset($values['form_name']) ||
$values['form_name'] == '') {
633 $this->errors
[] = _T('Missing required form!');
635 if (in_array($values['form_name'], array_keys(self
::getFormsNames()))) {
636 $this->form
= $values['form_name'];
638 $this->errors
[] = _T('Unknown form!');
643 $this->required
= $values['field_required'] ??
false;
645 if (count($this->errors
) === 0 && $this->isDuplicate()) {
646 $this->errors
[] = _T("- Field name already used.");
649 if ($this->hasWidth() && isset($values['field_width']) && trim($values['field_width']) != '') {
650 $this->width
= $values['field_width'];
653 if ($this->hasHeight() && isset($values['field_height']) && trim($values['field_height']) != '') {
654 $this->height
= $values['field_height'];
657 if ($this->hasSize() && isset($values['field_size']) && trim($values['field_size']) != '') {
658 $this->size
= $values['field_size'];
661 if (isset($values['field_repeat']) && trim($values['field_repeat']) != '') {
662 $this->repeat
= $values['field_repeat'];
665 if (isset($values['field_information']) && trim($values['field_information']) != '') {
667 $this->information
= $preferences->cleanHtmlValue($values['field_information']);
670 if ($this->hasFixedValues() && isset($values['fixed_values'])) {
672 foreach (explode("\n", $values['fixed_values']) as $val) {
674 $len = mb_strlen($val);
676 $fixed_values[] = $val;
677 if ($len > $this->size
) {
678 if ($this->old_size
=== null) {
679 $this->old_size
= $this->size
;
686 $this->values
= $fixed_values;
689 if ($this->id
== null) {
690 $this->index
= $this->getNewIndex();
693 if (count($this->errors
) === 0) {
701 * Store the field type
703 * @param array $values All values to check, basically the $_POST array
704 * after sending the form
708 public function store(array $values): bool
710 if (!$this->check($values)) {
714 $isnew = ($this->id
=== null);
715 if ($this->old_name
!== null) {
716 $this->deleteTranslation($this->old_name
);
717 $this->addTranslation($this->name
);
722 'field_name' => strip_tags($this->name
),
723 'field_perm' => $this->perm
,
724 'field_required' => $this->required
,
725 'field_width' => ($this->width
=== null ?
new Expression('NULL') : $this->width
),
726 'field_height' => ($this->height
=== null ?
new Expression('NULL') : $this->height
),
727 'field_size' => ($this->size
=== null ?
new Expression('NULL') : $this->size
),
728 'field_repeat' => ($this->repeat
=== null ?
new Expression('NULL') : $this->repeat
),
729 'field_form' => $this->form
,
730 'field_index' => $this->index
,
731 'field_information' => ($this->information
=== null ?
new Expression('NULL') : $this->information
)
734 if ($this->required
=== false) {
735 //Handle booleans for postgres ; bugs #18899 and #19354
736 $values['field_required'] = $this->zdb
->isPostgres() ?
'false' : 0;
740 $update = $this->zdb
->update(self
::TABLE
);
741 $update->set($values)->where([self
::PK
=> $this->id
]);
742 $this->zdb
->execute($update);
744 $values['field_type'] = $this->getType();
745 $insert = $this->zdb
->insert(self
::TABLE
);
746 $insert->values($values);
747 $this->zdb
->execute($insert);
749 $this->id
= $this->zdb
->getLastGeneratedValue($this);
751 if ($this->name
!= '') {
752 $this->addTranslation($this->name
);
755 } catch (Throwable
$e) {
757 'An error occurred storing field | ' . $e->getMessage(),
760 $this->errors
[] = _T("An error occurred storing the field.");
763 if (count($this->errors
) === 0 && $this->hasFixedValues()) {
764 $contents_table = self
::getFixedValuesTableName($this->id
, true);
767 $this->zdb
->drop(str_replace(PREFIX_DB
, '', $contents_table), true);
768 $field_size = ((int)$this->size
> 0) ?
$this->size
: 1;
769 $this->zdb
->db
->query(
770 'CREATE TABLE ' . $contents_table .
771 ' (id INTEGER NOT NULL,val varchar(' . $field_size .
773 \Laminas\Db\Adapter\Adapter
::QUERY_MODE_EXECUTE
775 } catch (Throwable
$e) {
777 'Unable to manage fields values table ' .
778 $contents_table . ' | ' . $e->getMessage(),
781 $this->errors
[] = _T("An error occurred creating field values table");
784 if (count($this->errors
) == 0 && is_array($this->values
)) {
785 $contents_table = self
::getFixedValuesTableName($this->id
);
787 $this->zdb
->connection
->beginTransaction();
789 $insert = $this->zdb
->insert($contents_table);
796 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($insert);
798 $cnt_values = count($this->values
);
799 for ($i = 0; $i < $cnt_values; $i++
) {
803 'val' => $this->values
[$i]
807 $this->zdb
->connection
->commit();
808 } catch (Throwable
$e) {
809 $this->zdb
->connection
->rollBack();
811 'Unable to store field ' . $this->id
. ' values (' .
812 $e->getMessage() . ')',
815 $this->warnings
[] = _T('An error occurred storing dynamic field values :(');
820 if (count($this->errors
) === 0) {
832 protected function getNewIndex(): int
834 $select = $this->zdb
->select(self
::TABLE
);
837 'idx' => new \Laminas\Db\Sql\
Expression('COUNT(*) + 1')
840 $select->where(['field_form' => $this->form
]);
841 $results = $this->zdb
->execute($select);
842 $result = $results->current();
848 * Is field duplicated?
852 public function isDuplicate(): bool
854 //let's consider field is duplicated, in case of future errors
857 $select = $this->zdb
->select(self
::TABLE
);
860 'cnt' => new \Laminas\Db\Sql\
Expression('COUNT(' . self
::PK
. ')')
864 'field_form' => $this->form
,
865 'field_name' => $this->name
869 if ($this->id
!== null) {
870 $select->where
->addPredicate(
871 new PredicateExpression(
872 'field_id NOT IN (?)',
878 $results = $this->zdb
->execute($select);
879 $result = $results->current();
884 } catch (Throwable
$e) {
886 'An error occurred checking field duplicity' . $e->getMessage(),
894 * Move a dynamic field
896 * @param string $action What to do (one of self::MOVE_*)
900 public function move(string $action): bool
902 if ($action !== self
::MOVE_UP
&& $action !== self
::MOVE_DOWN
) {
903 throw new \
RuntimeException(('Unknown action ' . $action));
907 $this->zdb
->connection
->beginTransaction();
909 $old_rank = $this->index
;
911 $direction = $action == self
::MOVE_UP ?
-1 : 1;
912 $new_rank = $old_rank +
$direction;
913 $update = $this->zdb
->update(self
::TABLE
);
915 'field_index' => $old_rank
917 'field_index' => $new_rank,
918 'field_form' => $this->form
920 $this->zdb
->execute($update);
922 $update = $this->zdb
->update(self
::TABLE
);
925 'field_index' => $new_rank
929 self
::PK
=> $this->id
932 $this->zdb
->execute($update);
933 $this->zdb
->connection
->commit();
936 } catch (Throwable
$e) {
937 $this->zdb
->connection
->rollBack();
939 'Unable to change field ' . $this->id
. ' rank | ' .
948 * Delete a dynamic field
952 public function remove(): bool
955 if ($this->hasFixedValues()) {
956 $contents_table = self
::getFixedValuesTableName($this->id
);
957 $this->zdb
->drop($contents_table);
960 $this->zdb
->connection
->beginTransaction();
961 $old_rank = $this->index
;
963 $update = $this->zdb
->update(self
::TABLE
);
966 'field_index' => new \Laminas\Db\Sql\
Expression('field_index-1')
969 ->greaterThan('field_index', $old_rank)
970 ->equalTo('field_form', $this->form
);
971 $this->zdb
->execute($update);
973 //remove associated values
975 $delete = $this->zdb
->delete(DynamicFieldsHandle
::TABLE
);
978 'field_id' => $this->id
,
979 'field_form' => $this->form
982 $this->zdb
->execute($delete);
983 } catch (Throwable
$e) {
984 throw new \
RuntimeException('Unable to remove associated values for field ' . $this->id
. '!');
989 $delete = $this->zdb
->delete(self
::TABLE
);
992 'field_id' => $this->id
,
993 'field_form' => $this->form
996 $this->zdb
->execute($delete);
997 } catch (Throwable
$e) {
998 throw new \
RuntimeException('Unable to remove field type ' . $this->id
. '!');
1001 $this->deleteTranslation($this->name
);
1003 $this->zdb
->connection
->commit();
1006 } catch (Throwable
$e) {
1007 if ($this->zdb
->connection
->inTransaction()) {
1008 //because of DROP autocommit on mysql...
1009 $this->zdb
->connection
->rollBack();
1012 'An error occurred deleting field | ' . $e->getMessage(),
1020 * Retrieve fields types names
1024 public static function getFieldsTypesNames(): array
1027 self
::SEPARATOR
=> _T("separator"),
1028 self
::TEXT
=> _T("free text"),
1029 self
::LINE
=> _T("single line"),
1030 self
::CHOICE
=> _T("choice"),
1031 self
::DATE
=> _T("date"),
1032 self
::BOOLEAN
=> _T("boolean"),
1033 self
::FILE
=> _T("file")
1043 public function getErrors(): array
1045 return $this->errors
;
1053 public function getWarnings(): array
1055 return $this->warnings
;