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 PERM_USER_WRITE
= 0;
85 public const PERM_ADMIN
= 1;
86 public const PERM_STAFF
= 2;
87 public const PERM_MANAGER
= 3;
88 public const PERM_USER_READ
= 4;
90 public const DEFAULT_MAX_FILE_SIZE
= 1024;
91 public const VALUES_FIELD_LENGTH
= 100;
93 protected $has_data = false;
94 protected $has_width = false;
95 protected $has_height = false;
96 protected $has_size = false;
97 protected $multi_valued = false;
98 protected $fixed_values = false;
99 protected $has_permissions = true;
118 * Default constructor
120 * @param Db $zdb Database instance
121 * @param mixed $args Arguments
123 public function __construct(Db
$zdb, $args = null)
129 } elseif ($args !== null && is_object($args)) {
130 $this->loadFromRs($args);
135 * Load field from its id
137 * @param Db $zdb Database instance
138 * @param int $id Field id
140 * @return DynamicField|false
142 public static function loadFieldType(Db
$zdb, $id)
145 $select = $zdb->select(self
::TABLE
);
146 $select->where('field_id = ' . $id);
148 $results = $zdb->execute($select);
149 $result = $results->current();
151 $field_type = $result->field_type
;
152 $field_type = self
::getFieldType($zdb, $field_type);
153 $field_type->loadFromRs($result);
156 } catch (Throwable
$e) {
158 __METHOD__
. ' | Unable to retrieve field `' . $id .
159 '` information | ' . $e->getMessage(),
168 * Get correct field type instance
170 * @param Db $zdb Database instance
171 * @param int $t Field type
172 * @param int $id Optional dynamic field id (to load data)
174 * @return DynamicField
176 public static function getFieldType(Db
$zdb, $t, $id = null)
180 case self
::SEPARATOR
:
181 $df = new Separator($zdb, $id);
184 $df = new Text($zdb, $id);
187 $df = new Line($zdb, $id);
190 $df = new Choice($zdb, $id);
193 $df = new Date($zdb, $id);
196 $df = new Boolean($zdb, $id);
199 $df = new File($zdb, $id);
202 throw new \
Exception('Unknown field type ' . $t . '!');
211 * @param integer $id Id
215 public function load($id)
218 $select = $this->zdb
->select(self
::TABLE
);
219 $select->where(self
::PK
. ' = ' . $id);
221 $results = $this->zdb
->execute($select);
222 $result = $results->current();
225 $this->loadFromRs($result);
227 } catch (Throwable
$e) {
229 'Unable to retrieve field type for field ' . $id . ' | ' .
237 * Load field type from a db ResultSet
239 * @param ResultSet $rs ResultSet
240 * @param boolean $values Whether to load values. Defaults to true
244 public function loadFromRs($rs, $values = true)
246 $this->id
= (int)$rs->field_id
;
247 $this->name
= $rs->field_name
;
248 $this->index
= (int)$rs->field_index
;
249 $this->perm
= (int)$rs->field_perm
;
250 $this->required
= ($rs->field_required
== 1 ?
true : false);
251 $this->width
= $rs->field_width
;
252 $this->height
= $rs->field_height
;
253 $this->repeat
= $rs->field_repeat
;
254 $this->size
= $rs->field_size
;
255 $this->form
= $rs->field_form
;
256 if ($values && $this->hasFixedValues()) {
257 $this->loadFixedValues();
262 * Retrieve fixed values table name
264 * @param integer $id Field ID
265 * @param boolean $prefixed Whether table name should be prefixed
269 public static function getFixedValuesTableName($id, $prefixed = false)
271 $name = 'field_contents_' . $id;
272 if ($prefixed === true) {
273 $name = PREFIX_DB
. $name;
279 * Returns an array of fixed valued for a field of type 'choice'.
283 private function loadFixedValues()
286 $val_select = $this->zdb
->select(
287 self
::getFixedValuesTableName($this->id
)
290 $val_select->columns(
296 $results = $this->zdb
->execute($val_select);
297 $this->values
= array();
299 foreach ($results as $val) {
300 $this->values
[] = $val->val
;
303 } catch (Throwable
$e) {
305 __METHOD__
. ' | ' . $e->getMessage(),
316 abstract public function getType();
319 * Get field type name
323 public function getTypeName()
325 $types = $this->getFieldsTypesNames();
326 if (isset($types[$this->getType()])) {
327 return $types[$this->getType()];
329 throw new \
RuntimeException(
330 'Unknow type ' . $this->getType()
336 * Does the field handle data?
340 public function hasData()
342 return $this->has_data
;
346 * Does the field has width?
350 public function hasWidth()
352 return $this->has_width
;
356 * Does the field has height?
360 public function hasHeight()
362 return $this->has_height
;
366 * Does the field has a size?
370 public function hasSize()
372 return $this->has_size
;
376 * Is the field multi valued?
380 public function isMultiValued()
382 return $this->multi_valued
;
386 * Does the field has fixed values?
390 public function hasFixedValues()
392 return $this->fixed_values
;
396 * Does the field require permissions?
400 public function hasPermissions()
402 return $this->has_permissions
;
411 public function getId()
417 * Get field Permissions
421 public function getPerm()
432 public function isRequired()
434 return $this->required
;
442 public function getWidth()
452 public function getHeight()
454 return $this->height
;
458 * Is current field repeatable?
462 public function isRepeatable()
464 return $this->repeat
!= null && trim($this->repeat
) != '' && (int)$this->repeat
>= 0;
468 * Get fields repetitions
470 * @return integer|boolean
472 public function getRepeat()
474 return $this->repeat
;
482 public function getSize()
492 public function getIndex()
498 * Retrieve permissions names for display
502 public static function getPermsNames()
505 self
::PERM_USER_WRITE
=> _T("User, read/write"),
506 self
::PERM_STAFF
=> _T("Staff member"),
507 self
::PERM_ADMIN
=> _T("Administrator"),
508 self
::PERM_MANAGER
=> _T("Group manager"),
509 self
::PERM_USER_READ
=> _T("User, read only")
514 * Retrieve forms names
518 public static function getFormsNames()
521 'adh' => _T("Members"),
522 'contrib' => _T("Contributions"),
523 'trans' => _T("Transactions")
530 * @param string $form_name Form name
534 public static function getFormTitle($form_name)
536 $names = self
::getFormsNames();
537 return $names[$form_name];
541 * Get permission name
545 public function getPermName()
547 $perms = self
::getPermsNames();
548 return $perms[$this->getPerm()];
556 public function getForm()
564 * @param boolean $imploded Whether to implode values
568 public function getValues($imploded = false)
570 if (!is_array($this->values
)) {
573 if ($imploded === true) {
574 return implode("\n", $this->values
);
576 return $this->values
;
581 * Check posted values validity
583 * @param array $values All values to check, basically the $_POST array
584 * after sending the form
588 public function check($values)
591 $this->warnings
= [];
594 (!isset($values['field_name']) ||
$values['field_name'] == '')
595 && get_class($this) != '\Galette\DynamicField\Separator'
597 $this->errors
[] = _T('Missing required field name!');
599 if ($this->old_name
=== null && $this->name
!== null && $this->name
!= $values['field_name']) {
600 $this->old_name
= $this->name
;
602 $this->name
= $values['field_name'];
605 if (!isset($values['field_perm']) ||
$values['field_perm'] === '') {
606 $this->errors
[] = _T('Missing required field permissions!');
608 if (in_array($values['field_perm'], array_keys(self
::getPermsNames()))) {
609 $this->perm
= $values['field_perm'];
611 $this->errors
[] = _T('Unknown permission!');
615 if ($this->id
=== null) {
616 if (!isset($values['form_name']) ||
$values['form_name'] == '') {
617 $this->errors
[] = _T('Missing required form!');
619 if (in_array($values['form_name'], array_keys(self
::getFormsNames()))) {
620 $this->form
= $values['form_name'];
622 $this->errors
[] = _T('Unknown form!');
627 $this->required
= $values['field_required'] ??
false;
629 if (count($this->errors
) === 0 && $this->isDuplicate($values['form_name'], $this->name
, $this->id
)) {
630 $this->errors
[] = _T("- Field name already used.");
633 if ($this->hasWidth() && isset($values['field_width']) && trim($values['field_width']) != '') {
634 $this->width
= $values['field_width'];
637 if ($this->hasHeight() && isset($values['field_height']) && trim($values['field_height']) != '') {
638 $this->height
= $values['field_height'];
641 if ($this->hasSize() && isset($values['field_size']) && trim($values['field_size']) != '') {
642 $this->size
= $values['field_size'];
645 if (isset($values['field_repeat']) && trim($values['field_repeat']) != '') {
646 $this->repeat
= $values['field_repeat'];
649 if ($this->hasFixedValues() && isset($values['fixed_values'])) {
651 foreach (explode("\n", $values['fixed_values']) as $val) {
653 $len = mb_strlen($val);
655 $fixed_values[] = $val;
656 if ($len > $this->size
) {
657 if ($this->old_size
=== null) {
658 $this->old_size
= $this->size
;
665 $this->values
= $fixed_values;
668 if ($this->id
== null) {
669 $this->index
= $this->getNewIndex();
672 if (count($this->errors
) === 0) {
680 * Store the field type
682 * @param array $values All values to check, basically the $_POST array
683 * after sending the form
687 public function store($values)
689 if (!$this->check($values)) {
693 $isnew = ($this->id
=== null);
694 if ($this->old_name
!== null) {
695 $this->deleteTranslation($this->old_name
);
696 $this->addTranslation($this->name
);
701 'field_name' => strip_tags($this->name
),
702 'field_perm' => $this->perm
,
703 'field_required' => $this->required
,
704 'field_width' => ($this->width
=== null ?
new Expression('NULL') : $this->width
),
705 'field_height' => ($this->height
=== null ?
new Expression('NULL') : $this->height
),
706 'field_size' => ($this->size
=== null ?
new Expression('NULL') : $this->size
),
707 'field_repeat' => ($this->repeat
=== null ?
new Expression('NULL') : $this->repeat
),
708 'field_form' => $this->form
,
709 'field_index' => $this->index
712 if ($this->required
=== false) {
713 //Handle booleans for postgres ; bugs #18899 and #19354
714 $values['field_required'] = $this->zdb
->isPostgres() ?
'false' : 0;
718 $update = $this->zdb
->update(self
::TABLE
);
719 $update->set($values)->where(
720 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()
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()
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(),
873 * Move a dynamic field
875 * @param string $action What to do (either 'up' or 'down')
879 public function move($action)
882 $this->zdb
->connection
->beginTransaction();
884 $old_rank = $this->index
;
886 $direction = $action == 'up' ?
-1 : 1;
887 $new_rank = $old_rank +
$direction;
888 $update = $this->zdb
->update(self
::TABLE
);
890 'field_index' => $old_rank
892 'field_index' => $new_rank,
893 'field_form' => $this->form
895 $this->zdb
->execute($update);
897 $update = $this->zdb
->update(self
::TABLE
);
900 'field_index' => $new_rank
904 self
::PK
=> $this->id
907 $this->zdb
->execute($update);
908 $this->zdb
->connection
->commit();
911 } catch (Throwable
$e) {
912 $this->zdb
->connection
->rollBack();
914 'Unable to change field ' . $this->id
. ' rank | ' .
923 * Delete a dynamic field
927 public function remove()
930 if ($this->hasFixedValues()) {
931 $contents_table = self
::getFixedValuesTableName($this->id
);
932 $this->zdb
->drop($contents_table);
935 $this->zdb
->connection
->beginTransaction();
936 $old_rank = $this->index
;
938 $update = $this->zdb
->update(self
::TABLE
);
941 'field_index' => new \Laminas\Db\Sql\
Expression('field_index-1')
944 ->greaterThan('field_index', $old_rank)
945 ->equalTo('field_form', $this->form
);
946 $this->zdb
->execute($update);
948 //remove associated values
949 $delete = $this->zdb
->delete(DynamicFieldsHandle
::TABLE
);
952 'field_id' => $this->id
,
953 'field_form' => $this->form
956 $result = $this->zdb
->execute($delete);
958 throw new \
RuntimeException('Unable to remove associated values for field ' . $this->id
. '!');
962 $delete = $this->zdb
->delete(self
::TABLE
);
965 'field_id' => $this->id
,
966 'field_form' => $this->form
969 $result = $this->zdb
->execute($delete);
971 throw new \
RuntimeException('Unable to remove field ' . $this->id
. '!');
974 $this->deleteTranslation($this->name
);
976 $this->zdb
->connection
->commit();
979 } catch (Throwable
$e) {
980 if ($this->zdb
->connection
->inTransaction()) {
981 //because of DROP autocommit on mysql...
982 $this->zdb
->connection
->rollBack();
985 'An error occurred deleting field | ' . $e->getMessage(),
993 * Retrieve fields types names
997 public static function getFieldsTypesNames()
1000 self
::SEPARATOR
=> _T("separator"),
1001 self
::TEXT
=> _T("free text"),
1002 self
::LINE
=> _T("single line"),
1003 self
::CHOICE
=> _T("choice"),
1004 self
::DATE
=> _T("date"),
1005 self
::BOOLEAN
=> _T("boolean"),
1006 self
::FILE
=> _T("file")
1016 public function getErrors()
1018 return $this->errors
;
1026 public function getWarnings()
1028 return $this->warnings
;