]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/DynamicFieldsHandle.php
Do not display separator or files in dynamic contributions fields; refs #1693
[galette.git] / galette / lib / Galette / Entity / DynamicFieldsHandle.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Dynamic fields handle, aggregating field descriptors and values
7 *
8 * PHP version 5
9 *
10 * Copyright © 2011-2023 The Galette Team
11 *
12 * This file is part of Galette (http://galette.tuxfamily.org).
13 *
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.
18 *
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.
23 *
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/>.
26 *
27 * @category Entity
28 * @package Galette
29 *
30 * @author Johan Cwiklinski <johan@x-tnd.be>
31 * @copyright 2011-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.7dev - 2011-06-20
35 */
36
37 namespace Galette\Entity;
38
39 use ArrayObject;
40 use Galette\DynamicFields\File;
41 use Galette\DynamicFields\Separator;
42 use Throwable;
43 use Analog\Analog;
44 use Laminas\Db\Adapter\Driver\StatementInterface;
45 use Galette\Core\Db;
46 use Galette\Core\Login;
47 use Galette\Core\Authentication;
48 use Galette\DynamicFields\DynamicField;
49 use Galette\Repository\DynamicFieldsSet;
50
51 /**
52 * Dynamic fields handle, aggregating field descriptors and values
53 *
54 * @name DynamicFieldsHandle
55 * @category Entity
56 * @package Galette
57 *
58 * @author Johan Cwiklinski <johan@x-tnd.be>
59 * @copyright 2011-2023 The Galette Team
60 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
61 * @link http://galette.tuxfamily.org
62 */
63
64 class DynamicFieldsHandle
65 {
66 public const TABLE = 'dynamic_fields';
67
68 private $dynamic_fields = [];
69 private $current_values = [];
70 private $form_name;
71 private $item_id;
72
73 private $errors = array();
74
75 private $zdb;
76 private $login;
77
78 private $insert_stmt;
79 private $update_stmt;
80 private $delete_stmt;
81
82 private $has_changed = false;
83
84 /**
85 * Default constructor
86 *
87 * @param Db $zdb Database instance
88 * @param Login $login Login instance
89 * @param mixed $instance Object instance
90 */
91 public function __construct(Db $zdb, Login $login, $instance = null)
92 {
93 $this->zdb = $zdb;
94 $this->login = $login;
95 if ($instance !== null) {
96 $this->load($instance);
97 }
98 }
99
100 /**
101 * Load dynamic fields values for specified object
102 *
103 * @param mixed $object Object instance
104 *
105 * @return bool
106 */
107 public function load($object)
108 {
109 $this->form_name = $object->getFormName();
110
111 try {
112 $this->item_id = $object->id;
113 $fields = new DynamicFieldsSet($this->zdb, $this->login);
114 $this->dynamic_fields = $fields->getList($this->form_name);
115
116 $results = $this->getCurrentFields();
117
118 if ($results->count() > 0) {
119 foreach ($results as $f) {
120 if (isset($this->dynamic_fields[$f->{DynamicField::PK}])) {
121 $field = $this->dynamic_fields[$f->{DynamicField::PK}];
122 if ($field->hasFixedValues()) {
123 $choices = $field->getValues();
124 if (!isset($choices[$f->field_val])) {
125 if ($idx = array_search($f->field_val, $choices)) {
126 //text has been stored (from CSV import?), but we want the index
127 $f->text_val = $f->field_val;
128 $f->field_val = $idx;
129 } else {
130 //something went wrong here :(
131 Analog::log(
132 'Dynamic choice value "' . $f->field_val . '" does not exists!',
133 Analog::WARNING
134 );
135 $f->text_val = $f->field_val;
136 }
137 } else {
138 $f->text_val = $choices[$f->field_val];
139 }
140 }
141 $this->current_values[$f->{DynamicField::PK}][] = array_filter(
142 (array)$f,
143 static function ($k) {
144 return $k != DynamicField::PK;
145 },
146 ARRAY_FILTER_USE_KEY
147 );
148 } else {
149 Analog::log(
150 'Dynamic values found for ' . get_class($object) . ' #' . $this->item_id .
151 '; but no dynamic field configured!',
152 Analog::WARNING
153 );
154 }
155 }
156 return true;
157 } else {
158 return false;
159 }
160 } catch (Throwable $e) {
161 Analog::log(
162 __METHOD__ . ' | ' . $e->getMessage(),
163 Analog::WARNING
164 );
165 throw $e;
166 }
167 }
168
169 /**
170 * Get errors
171 *
172 * @return array
173 */
174 public function getErrors(): array
175 {
176 return $this->errors;
177 }
178
179 /**
180 * Get fields
181 *
182 * @return array
183 */
184 public function getFields(): array
185 {
186 return $this->dynamic_fields;
187 }
188
189 /**
190 * Get fields for search pages
191 *
192 * @return array
193 */
194 public function getSearchFields(): array
195 {
196 $dynamics = $this->dynamic_fields;
197
198 foreach ($dynamics as $key => $field) {
199 if ($field instanceof Separator || $field instanceof File) {
200 unset($dynamics[$key]);
201 }
202 }
203
204 return $dynamics;
205 }
206
207 /**
208 * Get values
209 *
210 * @param integer $field Field ID
211 *
212 * @return array
213 */
214 public function getValues($field): array
215 {
216 if (!isset($this->current_values[$field])) {
217 $this->current_values[$field][] = [
218 'item_id' => $this->item_id,
219 'field_form' => $this->dynamic_fields[$field]->getForm(),
220 'val_index' => 1,
221 'field_val' => '',
222 'is_new' => true
223 ];
224 }
225 return $this->current_values[$field];
226 }
227
228 /**
229 * Set field value
230 *
231 * @param integer $item Item ID
232 * @param integer $field Field ID
233 * @param integer $index Value index
234 * @param mixed $value Value
235 *
236 * @return void
237 */
238 public function setValue($item, $field, $index, $value)
239 {
240 $idx = $index - 1;
241 $input = [
242 'item_id' => $item,
243 'field_form' => $this->dynamic_fields[$field]->getForm(),
244 'val_index' => $index,
245 'field_val' => $value,
246 ];
247
248 if (!isset($this->current_values[$field][$idx])) {
249 $input['is_new'] = true;
250 }
251
252 $this->current_values[$field][$idx] = $input;
253 }
254
255 /**
256 * Unset field value
257 *
258 * @param integer $item Item ID
259 * @param integer $field Field ID
260 * @param integer $index Value index
261 *
262 * @return void
263 */
264 public function unsetValue($item, $field, $index)
265 {
266 $idx = $index - 1;
267 if (isset($this->current_values[$field][$idx])) {
268 unset($this->current_values[$field][$idx]);
269 }
270 }
271
272 /**
273 * Store values
274 *
275 * @param integer $item_id Curent item id to use (will be used if current item_id is 0)
276 * @param boolean $transaction True if a transaction already exists
277 *
278 * @return boolean
279 */
280 public function storeValues($item_id = null, $transaction = false)
281 {
282 try {
283 if ($item_id !== null && ($this->item_id == null || $this->item_id == 0)) {
284 $this->item_id = $item_id;
285 }
286 if (!$transaction) {
287 $this->zdb->connection->beginTransaction();
288 }
289
290 $this->handleRemovals();
291
292 foreach ($this->current_values as $field_id => $values) {
293 foreach ($values as $value) {
294 $value[DynamicField::PK] = $field_id;
295 if ($value['item_id'] == 0) {
296 $value['item_id'] = $this->item_id;
297 }
298
299 if (isset($value['is_new'])) {
300 unset($value['is_new']);
301 $this->getInsertStatement()->execute($value);
302 $this->has_changed = true;
303 } else {
304 $params = [
305 'field_val' => $value['field_val'],
306 'val_index' => $value['val_index'],
307 'item_id' => $value['item_id'],
308 'field_id' => $value['field_id'],
309 'field_form' => $value['field_form'],
310 'old_val_index' => $value['old_val_index'] ?? $value['val_index'] //:old_val_index
311 ];
312 $this->getUpdateStatement()->execute($params);
313 $this->has_changed = true;
314 }
315 }
316 }
317
318 if (!$transaction) {
319 $this->zdb->connection->commit();
320 }
321 return true;
322 } catch (Throwable $e) {
323 if (!$transaction) {
324 $this->zdb->connection->rollBack();
325 }
326 Analog::log(
327 'An error occurred storing dynamic field. Form name: ' . $this->form_name .
328 ' | Error was: ' . $e->getMessage(),
329 Analog::ERROR
330 );
331 throw $e;
332 } finally {
333 unset(
334 $this->update_stmt,
335 $this->insert_stmt
336 );
337 }
338 }
339
340 /**
341 * Get (and prepare if not done yet) insert statement
342 *
343 * @return StatementInterface
344 */
345 private function getInsertStatement(): StatementInterface
346 {
347 if (!isset($this->insert_stmt)) {
348 $insert = $this->zdb->insert(self::TABLE);
349 $insert->values([
350 'item_id' => ':item_id',
351 'field_id' => ':field_id',
352 'field_form' => ':field_form',
353 'val_index' => ':val_index',
354 'field_val' => ':field_val'
355 ]);
356 $this->insert_stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
357 }
358 return $this->insert_stmt;
359 }
360
361 /**
362 * Get (and prepare if not done yet) update statement
363 *
364 * @return StatementInterface
365 */
366 private function getUpdateStatement(): StatementInterface
367 {
368 if (!isset($this->update_stmt)) {
369 $update = $this->zdb->update(self::TABLE);
370 $update->set([
371 'field_val' => ':field_val',
372 'val_index' => ':val_index'
373 ])->where([
374 'item_id' => ':item_id',
375 'field_id' => ':field_id',
376 'field_form' => ':field_form',
377 'val_index' => ':old_val_index'
378 ]);
379 $this->update_stmt = $this->zdb->sql->prepareStatementForSqlObject($update);
380 }
381 return $this->update_stmt;
382 }
383
384 /**
385 * Handle values that have been removed
386 *
387 * @return void
388 */
389 private function handleRemovals()
390 {
391 $fields = new DynamicFieldsSet($this->zdb, $this->login);
392 $this->dynamic_fields = $fields->getList($this->form_name);
393
394 $results = $this->getCurrentFields();
395
396 $fromdb = [];
397 if ($results->count() > 0) {
398 foreach ($results as $result) {
399 $fromdb[$result->field_id . '_' . $result->val_index] = [
400 'item_id' => $this->item_id,
401 'field_form' => $this->form_name,
402 'field_id' => $result->field_id,
403 'val_index' => $result->val_index
404 ];
405 }
406 }
407
408 if (!count($fromdb)) {
409 //no entry in database, nothing to do.
410 return;
411 }
412
413 foreach ($this->current_values as $field_id => $values) {
414 foreach ($values as $value) {
415 $key = $field_id . '_' . $value['val_index'];
416 if (isset($fromdb[$key])) {
417 unset($fromdb[$key]);
418 }
419 }
420 }
421
422 if (count($fromdb)) {
423 foreach ($fromdb as $entry) {
424 if ($this->delete_stmt === null) {
425 $delete = $this->zdb->delete(self::TABLE);
426 $delete->where([
427 'item_id' => ':item_id',
428 'field_form' => ':field_form',
429 'field_id' => ':field_id',
430 'val_index' => ':val_index'
431 ]);
432 $this->delete_stmt = $this->zdb->sql->prepareStatementForSqlObject($delete);
433 }
434 $this->delete_stmt->execute($entry);
435 //update val index
436 $field_id = $entry['field_id'];
437 if (
438 isset($this->current_values[$field_id])
439 && count($this->current_values[$field_id])
440 ) {
441 $val_index = (int)$entry['val_index'];
442 foreach ($this->current_values[$field_id] as &$current) {
443 if ((int)$current['val_index'] === $val_index + 1) {
444 $current['val_index'] = $val_index;
445 ++$val_index;
446 $current['old_val_index'] = $val_index;
447 }
448 }
449 }
450 }
451 $this->has_changed = true;
452 }
453 }
454
455 /**
456 * Is there any change in dynamic fields?
457 *
458 * @return boolean
459 */
460 public function hasChanged()
461 {
462 return $this->has_changed;
463 }
464
465 /**
466 * Remove values
467 *
468 * @param integer $item_id Curent item id to use (will be used if current item_id is 0)
469 * @param boolean $transaction True if a transaction already exists
470 *
471 * @return boolean
472 */
473 public function removeValues($item_id = null, $transaction = false)
474 {
475 try {
476 if ($item_id !== null && ($this->item_id == null || $this->item_id == 0)) {
477 $this->item_id = $item_id;
478 }
479 if (!$transaction) {
480 $this->zdb->connection->beginTransaction();
481 }
482
483 $delete = $this->zdb->delete(self::TABLE);
484 $delete->where(
485 array(
486 'item_id' => $this->item_id,
487 'field_form' => $this->form_name
488 )
489 );
490 $this->zdb->execute($delete);
491
492 if (!$transaction) {
493 $this->zdb->connection->commit();
494 }
495 return true;
496 } catch (Throwable $e) {
497 if (!$transaction) {
498 $this->zdb->connection->rollBack();
499 }
500 Analog::log(
501 'An error occurred removing dynamic field. Form name: ' . $this->form_name .
502 ' | Error was: ' . $e->getMessage(),
503 Analog::ERROR
504 );
505 throw $e;
506 }
507 }
508
509 /**
510 * Get current fields resultset
511 *
512 * @return ArrayObject
513 */
514 protected function getCurrentFields()
515 {
516 $select = $this->zdb->select(self::TABLE, 'd');
517 $select->join(
518 array('t' => PREFIX_DB . DynamicField::TABLE),
519 'd.' . DynamicField::PK . '=t.' . DynamicField::PK,
520 array('field_id')
521 )->where(
522 array(
523 'item_id' => $this->item_id,
524 'd.field_form' => $this->form_name
525 )
526 );
527
528 /** only load values for accessible fields*/
529 $accessible_fields = [];
530 $access_level = $this->login->getAccessLevel();
531
532 foreach ($this->dynamic_fields as $field) {
533 $perm = $field->getPerm();
534 if (
535 ($perm == DynamicField::PERM_MANAGER &&
536 $access_level < Authentication::ACCESS_MANAGER) ||
537 ($perm == DynamicField::PERM_STAFF &&
538 $access_level < Authentication::ACCESS_STAFF) ||
539 ($perm == DynamicField::PERM_ADMIN &&
540 $access_level < Authentication::ACCESS_ADMIN)
541 ) {
542 continue;
543 }
544 $accessible_fields[] = $field->getId();
545 }
546
547 if (count($accessible_fields)) {
548 $select->where->in('d.' . DynamicField::PK, $accessible_fields);
549 }
550
551 $results = $this->zdb->execute($select);
552 return $results;
553 }
554 }