3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
6 * Abstract dynamic field
10 * Copyright © 2012-2021 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-2021 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
;
42 use Galette\Entity\DynamicFieldsHandle
;
43 use Galette\Features\Translatable
;
44 use Galette\Features\I18n
;
45 use Laminas\Db\Sql\Expression
;
46 use Laminas\Db\Sql\Predicate\Expression
as PredicateExpression
;
49 * Abstract dynamic field
52 * @category DynamicFields
55 * @author Johan Cwiklinski <johan@x-tnd.be>
56 * @copyright 2012-2014 The Galette Team
57 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
58 * @link http://galette.tuxfamily.org
61 abstract class DynamicField
66 public const TABLE
= 'field_types';
67 public const PK
= 'field_id';
69 /** Separator field */
70 public const SEPARATOR
= 0;
71 /** Simple text field */
72 public const TEXT
= 1;
74 public const LINE
= 2;
75 /** Choice field (listbox) */
76 public const CHOICE
= 3;
78 public const DATE
= 4;
79 /** Boolean field (checkbox) */
80 public const BOOLEAN
= 5;
81 /** File field (upload) */
82 public const FILE
= 6;
84 public const MOVE_UP
= 'up';
85 public const MOVE_DOWN
= 'down';
87 public const PERM_USER_WRITE
= 0;
88 public const PERM_ADMIN
= 1;
89 public const PERM_STAFF
= 2;
90 public const PERM_MANAGER
= 3;
91 public const PERM_USER_READ
= 4;
93 public const DEFAULT_MAX_FILE_SIZE
= 1024;
94 public const VALUES_FIELD_LENGTH
= 100;
96 protected $has_data = false;
97 protected $has_width = false;
98 protected $has_height = false;
99 protected $has_size = false;
100 protected $multi_valued = false;
101 protected $fixed_values = false;
102 protected $has_permissions = true;
116 protected $errors = [];
121 * Default constructor
123 * @param Db $zdb Database instance
124 * @param mixed $args Arguments
126 public function __construct(Db
$zdb, $args = null)
132 } elseif (is_object($args)) {
133 $this->loadFromRs($args);
138 * Load field from its id
140 * @param Db $zdb Database instance
141 * @param int $id Field id
143 * @return DynamicField|false
145 public static function loadFieldType(Db
$zdb, int $id)
148 $select = $zdb->select(self
::TABLE
);
149 $select->where(['field_id' => $id]);
151 $results = $zdb->execute($select);
152 $result = $results->current();
154 $field_type = $result->field_type
;
155 $field_type = self
::getFieldType($zdb, $field_type);
156 $field_type->loadFromRs($result);
159 } catch (Throwable
$e) {
161 __METHOD__
. ' | Unable to retrieve field `' . $id .
162 '` information | ' . $e->getMessage(),
171 * Get correct field type instance
173 * @param Db $zdb Database instance
174 * @param int $t Field type
175 * @param int|null $id Optional dynamic field id (to load data)
177 * @return DynamicField
179 public static function getFieldType(Db
$zdb, int $t, int $id = null)
183 case self
::SEPARATOR
:
184 $df = new Separator($zdb, $id);
187 $df = new Text($zdb, $id);
190 $df = new Line($zdb, $id);
193 $df = new Choice($zdb, $id);
196 $df = new Date($zdb, $id);
199 $df = new Boolean($zdb, $id);
202 $df = new File($zdb, $id);
205 throw new \
Exception('Unknown field type ' . $t . '!');
213 * @param integer $id Id
217 public function load(int $id): void
220 $select = $this->zdb
->select(self
::TABLE
);
221 $select->where([self
::PK
=> $id]);
223 $results = $this->zdb
->execute($select);
224 $result = $results->current();
227 $this->loadFromRs($result);
229 } catch (Throwable
$e) {
231 'Unable to retrieve field type for field ' . $id . ' | ' .
239 * Load field type from a db ResultSet
241 * @param ResultSet $rs ResultSet
242 * @param boolean $values Whether to load values. Defaults to true
246 public function loadFromRs($rs, bool $values = true): void
248 $this->id
= (int)$rs->field_id
;
249 $this->name
= $rs->field_name
;
250 $this->index
= (int)$rs->field_index
;
251 $this->perm
= (int)$rs->field_perm
;
252 $this->required
= $rs->field_required
== 1;
253 $this->width
= $rs->field_width
;
254 $this->height
= $rs->field_height
;
255 $this->repeat
= (int)$rs->field_repeat
;
256 $this->size
= $rs->field_size
;
257 $this->form
= $rs->field_form
;
258 if ($values && $this->hasFixedValues()) {
259 $this->loadFixedValues();
264 * Retrieve fixed values table name
266 * @param integer $id Field ID
267 * @param boolean $prefixed Whether table name should be prefixed
271 public static function getFixedValuesTableName(int $id, bool $prefixed = false): string
273 $name = 'field_contents_' . $id;
274 if ($prefixed === true) {
275 $name = PREFIX_DB
. $name;
281 * Returns an array of fixed valued for a field of type 'choice'.
285 private function loadFixedValues()
288 $val_select = $this->zdb
->select(
289 self
::getFixedValuesTableName($this->id
)
292 $val_select->columns(
298 $results = $this->zdb
->execute($val_select);
299 $this->values
= array();
301 foreach ($results as $val) {
302 $this->values
[] = $val->val
;
305 } catch (Throwable
$e) {
307 __METHOD__
. ' | ' . $e->getMessage(),
318 abstract public function getType(): int;
321 * Get field type name
325 public function getTypeName(): string
327 $types = $this->getFieldsTypesNames();
328 if (isset($types[$this->getType()])) {
329 return $types[$this->getType()];
331 throw new \
RuntimeException(
332 'Unknown type ' . $this->getType()
338 * Does the field handle data?
342 public function hasData(): bool
344 return (bool)$this->has_data
;
348 * Does the field has width?
352 public function hasWidth(): bool
354 return (bool)$this->has_width
;
358 * Does the field has height?
362 public function hasHeight(): bool
364 return (bool)$this->has_height
;
368 * Does the field has a size?
372 public function hasSize(): bool
374 return (bool)$this->has_size
;
378 * Is the field multivalued?
382 public function isMultiValued(): bool
384 return (bool)$this->multi_valued
;
388 * Does the field has fixed values?
392 public function hasFixedValues(): bool
394 return (bool)$this->fixed_values
;
398 * Does the field require permissions?
402 public function hasPermissions(): bool
404 return (bool)$this->has_permissions
;
411 * @return integer|null
413 public function getId(): ?
int
419 * Get field Permissions
421 * @return integer|null
423 public function getPerm(): ?
int
434 public function isRequired(): bool
436 return (bool)$this->required
;
442 * @return integer|null
444 public function getWidth(): ?
int
452 * @return integer|null
454 public function getHeight(): ?
int
456 return $this->height
;
460 * Is current field repeatable?
464 public function isRepeatable(): bool
466 return $this->repeat
!= null && trim($this->repeat
) != '' && (int)$this->repeat
>= 0;
470 * Get fields repetitions
472 * @return integer|null
474 public function getRepeat(): ?
int
476 return $this->repeat
;
482 * @return integer|null
484 public function getSize(): ?
int
492 * @return integer|null
494 public function getIndex(): ?
int
500 * Retrieve permissions names for display
504 public static function getPermsNames(): array
507 self
::PERM_USER_WRITE
=> _T("User, read/write"),
508 self
::PERM_STAFF
=> _T("Staff member"),
509 self
::PERM_ADMIN
=> _T("Administrator"),
510 self
::PERM_MANAGER
=> _T("Group manager"),
511 self
::PERM_USER_READ
=> _T("User, read only")
516 * Retrieve forms names
520 public static function getFormsNames(): array
523 'adh' => _T("Members"),
524 'contrib' => _T("Contributions"),
525 'trans' => _T("Transactions")
532 * @param string $form_name Form name
536 public static function getFormTitle(string $form_name): string
538 $names = self
::getFormsNames();
539 return $names[$form_name];
543 * Get permission name
547 public function getPermName(): string
549 $perms = self
::getPermsNames();
550 return $perms[$this->getPerm()];
558 public function getForm(): string
566 * @param boolean $imploded Whether to implode values
568 * @return array|string|false
570 public function getValues(bool $imploded = false)
572 if (!is_array($this->values
)) {
575 if ($imploded === true) {
576 return implode("\n", $this->values
);
578 return $this->values
;
583 * Check posted values validity
585 * @param array $values All values to check, basically the $_POST array
586 * after sending the form
590 public function check(array $values)
593 $this->warnings
= [];
596 (!isset($values['field_name']) ||
$values['field_name'] == '')
597 && get_class($this) != '\Galette\DynamicField\Separator'
599 $this->errors
[] = _T('Missing required field name!');
601 if ($this->old_name
=== null && $this->name
!== null && $this->name
!= $values['field_name']) {
602 $this->old_name
= $this->name
;
604 $this->name
= $values['field_name'];
607 if (!isset($values['field_perm']) ||
$values['field_perm'] === '') {
608 $this->errors
[] = _T('Missing required field permissions!');
610 if (in_array($values['field_perm'], array_keys(self
::getPermsNames()))) {
611 $this->perm
= $values['field_perm'];
613 $this->errors
[] = _T('Unknown permission!');
617 if ($this->id
=== null) {
618 if (!isset($values['form_name']) ||
$values['form_name'] == '') {
619 $this->errors
[] = _T('Missing required form!');
621 if (in_array($values['form_name'], array_keys(self
::getFormsNames()))) {
622 $this->form
= $values['form_name'];
624 $this->errors
[] = _T('Unknown form!');
629 $this->required
= $values['field_required'] ??
false;
631 if (count($this->errors
) === 0 && $this->isDuplicate($values['form_name'], $this->name
, $this->id
)) {
632 $this->errors
[] = _T("- Field name already used.");
635 if ($this->hasWidth() && isset($values['field_width']) && trim($values['field_width']) != '') {
636 $this->width
= $values['field_width'];
639 if ($this->hasHeight() && isset($values['field_height']) && trim($values['field_height']) != '') {
640 $this->height
= $values['field_height'];
643 if ($this->hasSize() && isset($values['field_size']) && trim($values['field_size']) != '') {
644 $this->size
= $values['field_size'];
647 if (isset($values['field_repeat']) && trim($values['field_repeat']) != '') {
648 $this->repeat
= $values['field_repeat'];
651 if ($this->hasFixedValues() && isset($values['fixed_values'])) {
653 foreach (explode("\n", $values['fixed_values']) as $val) {
655 $len = mb_strlen($val);
657 $fixed_values[] = $val;
658 if ($len > $this->size
) {
659 if ($this->old_size
=== null) {
660 $this->old_size
= $this->size
;
667 $this->values
= $fixed_values;
670 if ($this->id
== null) {
671 $this->index
= $this->getNewIndex();
674 if (count($this->errors
) === 0) {
682 * Store the field type
684 * @param array $values All values to check, basically the $_POST array
685 * after sending the form
689 public function store(array $values): bool
691 if (!$this->check($values)) {
695 $isnew = ($this->id
=== null);
696 if ($this->old_name
!== null) {
697 $this->deleteTranslation($this->old_name
);
698 $this->addTranslation($this->name
);
703 'field_name' => strip_tags($this->name
),
704 'field_perm' => $this->perm
,
705 'field_required' => $this->required
,
706 'field_width' => ($this->width
=== null ?
new Expression('NULL') : $this->width
),
707 'field_height' => ($this->height
=== null ?
new Expression('NULL') : $this->height
),
708 'field_size' => ($this->size
=== null ?
new Expression('NULL') : $this->size
),
709 'field_repeat' => ($this->repeat
=== null ?
new Expression('NULL') : $this->repeat
),
710 'field_form' => $this->form
,
711 'field_index' => $this->index
714 if ($this->required
=== false) {
715 //Handle booleans for postgres ; bugs #18899 and #19354
716 $values['field_required'] = $this->zdb
->isPostgres() ?
'false' : 0;
720 $update = $this->zdb
->update(self
::TABLE
);
721 $update->set($values)->where([self
::PK
=> $this->id
]);
722 $this->zdb
->execute($update);
724 $values['field_type'] = $this->getType();
725 $insert = $this->zdb
->insert(self
::TABLE
);
726 $insert->values($values);
727 $this->zdb
->execute($insert);
729 $this->id
= $this->zdb
->getLastGeneratedValue($this);
731 if ($this->name
!= '') {
732 $this->addTranslation($this->name
);
735 } catch (Throwable
$e) {
737 'An error occurred storing field | ' . $e->getMessage(),
740 $this->errors
[] = _T("An error occurred storing the field.");
743 if (count($this->errors
) === 0 && $this->hasFixedValues()) {
744 $contents_table = self
::getFixedValuesTableName($this->id
, true);
747 $this->zdb
->drop(str_replace(PREFIX_DB
, '', $contents_table), true);
748 $field_size = ((int)$this->size
> 0) ?
$this->size
: 1;
749 $this->zdb
->db
->query(
750 'CREATE TABLE ' . $contents_table .
751 ' (id INTEGER NOT NULL,val varchar(' . $field_size .
753 \Laminas\Db\Adapter\Adapter
::QUERY_MODE_EXECUTE
755 } catch (Throwable
$e) {
757 'Unable to manage fields values table ' .
758 $contents_table . ' | ' . $e->getMessage(),
761 $this->errors
[] = _T("An error occurred creating field values table");
764 if (count($this->errors
) == 0 && is_array($this->values
)) {
765 $contents_table = self
::getFixedValuesTableName($this->id
);
767 $this->zdb
->connection
->beginTransaction();
769 $insert = $this->zdb
->insert($contents_table);
776 $stmt = $this->zdb
->sql
->prepareStatementForSqlObject($insert);
778 $cnt_values = count($this->values
);
779 for ($i = 0; $i < $cnt_values; $i++
) {
783 'val' => $this->values
[$i]
787 $this->zdb
->connection
->commit();
788 } catch (Throwable
$e) {
789 $this->zdb
->connection
->rollBack();
791 'Unable to store field ' . $this->id
. ' values (' .
792 $e->getMessage() . ')',
795 $this->warnings
[] = _T('An error occurred storing dynamic field values :(');
800 if (count($this->errors
) === 0) {
812 protected function getNewIndex(): int
814 $select = $this->zdb
->select(self
::TABLE
);
817 'idx' => new \Laminas\Db\Sql\
Expression('COUNT(*) + 1')
820 $select->where(['field_form' => $this->form
]);
821 $results = $this->zdb
->execute($select);
822 $result = $results->current();
828 * Is field duplicated?
832 public function isDuplicate(): bool
834 //let's consider field is duplicated, in case of future errors
837 $select = $this->zdb
->select(self
::TABLE
);
840 'cnt' => new \Laminas\Db\Sql\
Expression('COUNT(' . self
::PK
. ')')
844 'field_form' => $this->form
,
845 'field_name' => $this->name
849 if ($this->id
!== null) {
850 $select->where
->addPredicate(
851 new PredicateExpression(
852 'field_id NOT IN (?)',
858 $results = $this->zdb
->execute($select);
859 $result = $results->current();
864 } catch (Throwable
$e) {
866 'An error occurred checking field duplicity' . $e->getMessage(),
874 * Move a dynamic field
876 * @param string $action What to do (one of self::MOVE_*)
880 public function move(string $action): bool
882 if ($action !== self
::MOVE_UP
&& $action !== self
::MOVE_DOWN
) {
883 throw new \
RuntimeException(('Unknown action ' . $action));
887 $this->zdb
->connection
->beginTransaction();
889 $old_rank = $this->index
;
891 $direction = $action == self
::MOVE_UP ?
-1 : 1;
892 $new_rank = $old_rank +
$direction;
893 $update = $this->zdb
->update(self
::TABLE
);
895 'field_index' => $old_rank
897 'field_index' => $new_rank,
898 'field_form' => $this->form
900 $this->zdb
->execute($update);
902 $update = $this->zdb
->update(self
::TABLE
);
905 'field_index' => $new_rank
909 self
::PK
=> $this->id
912 $this->zdb
->execute($update);
913 $this->zdb
->connection
->commit();
916 } catch (Throwable
$e) {
917 $this->zdb
->connection
->rollBack();
919 'Unable to change field ' . $this->id
. ' rank | ' .
928 * Delete a dynamic field
932 public function remove(): bool
935 if ($this->hasFixedValues()) {
936 $contents_table = self
::getFixedValuesTableName($this->id
);
937 $this->zdb
->drop($contents_table);
940 $this->zdb
->connection
->beginTransaction();
941 $old_rank = $this->index
;
943 $update = $this->zdb
->update(self
::TABLE
);
946 'field_index' => new \Laminas\Db\Sql\
Expression('field_index-1')
949 ->greaterThan('field_index', $old_rank)
950 ->equalTo('field_form', $this->form
);
951 $this->zdb
->execute($update);
953 //remove associated values
954 $delete = $this->zdb
->delete(DynamicFieldsHandle
::TABLE
);
957 'field_id' => $this->id
,
958 'field_form' => $this->form
961 $result = $this->zdb
->execute($delete);
963 throw new \
RuntimeException('Unable to remove associated values for field ' . $this->id
. '!');
967 $delete = $this->zdb
->delete(self
::TABLE
);
970 'field_id' => $this->id
,
971 'field_form' => $this->form
974 $result = $this->zdb
->execute($delete);
976 throw new \
RuntimeException('Unable to remove field ' . $this->id
. '!');
979 $this->deleteTranslation($this->name
);
981 $this->zdb
->connection
->commit();
984 } catch (Throwable
$e) {
985 if ($this->zdb
->connection
->inTransaction()) {
986 //because of DROP autocommit on mysql...
987 $this->zdb
->connection
->rollBack();
990 'An error occurred deleting field | ' . $e->getMessage(),
998 * Retrieve fields types names
1002 public static function getFieldsTypesNames(): array
1005 self
::SEPARATOR
=> _T("separator"),
1006 self
::TEXT
=> _T("free text"),
1007 self
::LINE
=> _T("single line"),
1008 self
::CHOICE
=> _T("choice"),
1009 self
::DATE
=> _T("date"),
1010 self
::BOOLEAN
=> _T("boolean"),
1011 self
::FILE
=> _T("file")
1021 public function getErrors(): array
1023 return $this->errors
;
1031 public function getWarnings(): array
1033 return $this->warnings
;