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