]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Entitled.php
767364d4ac180581eff9a486d6135ac157753cec
[galette.git] / galette / lib / Galette / Entity / Entitled.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Entitleds handling
7 *
8 * PHP version 5
9 *
10 * Copyright © 2007-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 2007-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 - 2007-10-27
35 */
36
37 namespace Galette\Entity;
38
39 use ArrayObject;
40 use Throwable;
41 use Analog\Analog;
42 use Laminas\Db\Sql\Expression;
43 use Galette\Core\Db;
44 use Galette\Features\I18n;
45
46 /**
47 * Entitled handling. Manage:
48 * - id
49 * - label
50 * - extra (that may differ from one entity to another)
51 *
52 * @category Entity
53 * @name Entitled
54 * @package Galette
55 * @author Johan Cwiklinski <johan@x-tnd.be>
56 * @copyright 2007-2023 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
59 * @since Available since 0.7dev - 2007-10-27
60 *
61 * @property integer $id
62 * @property string $label
63 * @property string $libelle
64 * @property mixed $third
65 * @property mixed $extension
66 */
67
68 abstract class Entitled
69 {
70 use I18n;
71
72 public const ID_NOT_EXITS = -1;
73
74 private $zdb;
75 private $table;
76 private $fpk;
77 private $flabel;
78 private $fthird;
79 private $used;
80
81 public static $fields;
82 protected static $defaults;
83
84 /** @var string|false */
85 protected $order_field = false;
86
87 private $id;
88 private $label;
89 private $third;
90
91 private $errors = array();
92
93 /**
94 * Default constructor
95 *
96 * @param Db $zdb Database
97 * @param string $table Table name
98 * @param string $fpk Primary key field name
99 * @param string $flabel Label fields name
100 * @param string $fthird The third field name
101 * @param string $used Table name for isUsed function
102 * @param mixed $args Either an int or a resultset to load
103 */
104 public function __construct(Db $zdb, $table, $fpk, $flabel, $fthird, $used, $args = null)
105 {
106 $this->zdb = $zdb;
107 $this->table = $table;
108 $this->fpk = $fpk;
109 $this->flabel = $flabel;
110 $this->fthird = $fthird;
111 $this->used = $used;
112 if (is_int($args)) {
113 $this->load($args);
114 } elseif (is_object($args)) {
115 $this->loadFromRS($args);
116 }
117 }
118
119 /**
120 * Loads an entry from its id
121 *
122 * @param int $id Entry ID
123 *
124 * @return boolean true if query succeed, false otherwise
125 */
126 public function load($id)
127 {
128 try {
129 $select = $this->zdb->select($this->table);
130 $select->where([$this->fpk => $id]);
131
132 $results = $this->zdb->execute($select);
133 if ($results->count() > 0) {
134 /** @var ArrayObject $result */
135 $result = $results->current();
136 $this->loadFromRS($result);
137
138 return true;
139 } else {
140 Analog::log(
141 'Unknown ID ' . $id,
142 Analog::ERROR
143 );
144 return false;
145 }
146 } catch (Throwable $e) {
147 Analog::log(
148 'Cannot load ' . $this->getType() . ' from id `' . $id . '` | ' .
149 $e->getMessage(),
150 Analog::WARNING
151 );
152 throw $e;
153 }
154 }
155
156 /**
157 * Populate object from a resultset row
158 *
159 * @param ArrayObject $r the resultset row
160 *
161 * @return void
162 */
163 private function loadFromRS(ArrayObject $r)
164 {
165 $pk = $this->fpk;
166 $this->id = $r->$pk;
167 $flabel = $this->flabel;
168 $this->label = $r->$flabel;
169 $fthird = $this->fthird;
170 $this->third = $r->$fthird;
171 }
172
173 /**
174 * Set defaults at install time
175 *
176 * @return boolean
177 * @throws Throwable
178 */
179 public function installInit()
180 {
181 $class = get_class($this);
182
183 try {
184 //first, we drop all values
185 $delete = $this->zdb->delete($this->table);
186 $this->zdb->execute($delete);
187
188 $values = array();
189 foreach ($class::$fields as $key => $f) {
190 $values[$f] = ':' . $key;
191 }
192
193 $insert = $this->zdb->insert($this->table);
194 $insert->values($values);
195 $stmt = $this->zdb->sql->prepareStatementForSqlObject($insert);
196
197 $this->zdb->handleSequence(
198 $this->table,
199 count(static::$defaults)
200 );
201
202 $fnames = array_values($values);
203 foreach ($class::$defaults as $d) {
204 $val = null;
205 if (isset($d['priority'])) {
206 $val = $d['priority'];
207 } else {
208 $val = $d['extension'];
209 }
210
211 $stmt->execute(
212 array(
213 $fnames[0] => $d['id'],
214 $fnames[1] => $d['libelle'],
215 $fnames[2] => $val
216 )
217 );
218 }
219
220 Analog::log(
221 'Defaults (' . $this->getType() .
222 ') were successfully stored into database.',
223 Analog::INFO
224 );
225 return true;
226 } catch (Throwable $e) {
227 Analog::log(
228 'Unable to initialize defaults (' . $this->getType() . ').' .
229 $e->getMessage(),
230 Analog::WARNING
231 );
232 throw $e;
233 }
234 }
235
236 /**
237 * Get list in an array built as:
238 * $array[id] = "translated label"
239 *
240 * @param boolean|null $extent Filter on (non) cotisations types
241 *
242 * @return array|false
243 */
244 public function getList(bool $extent = null)
245 {
246 $list = array();
247
248 try {
249 $select = $this->zdb->select($this->table);
250 $fields = array($this->fpk, $this->flabel);
251 if (
252 $this->order_field !== false
253 && $this->order_field !== $this->fpk
254 && $this->order_field !== $this->flabel
255 ) {
256 $fields[] = $this->order_field;
257 }
258 $select->quantifier('DISTINCT');
259 $select->columns($fields);
260
261 if ($this->order_field !== false) {
262 $select->order($this->order_field);
263 }
264 if ($extent !== null) {
265 if ($extent === true) {
266 $select->where(array($this->fthird => new Expression('true')));
267 } elseif ($extent === false) {
268 $select->where(array($this->fthird => new Expression('false')));
269 }
270 }
271
272 $results = $this->zdb->execute($select);
273
274 foreach ($results as $r) {
275 $fpk = $this->fpk;
276 $flabel = $this->flabel;
277 $list[$r->$fpk] = _T($r->$flabel);
278 }
279 return $list;
280 } catch (Throwable $e) {
281 Analog::log(
282 __METHOD__ . ' | ' . $e->getMessage(),
283 Analog::ERROR
284 );
285 throw $e;
286 }
287 }
288
289 /**
290 * Complete list
291 *
292 * @return array of all objects if succeeded, false otherwise
293 */
294 public function getCompleteList()
295 {
296 $list = array();
297
298 try {
299 $select = $this->zdb->select($this->table);
300 if ($this->order_field !== false) {
301 $select->order(array($this->order_field, $this->fpk));
302 }
303
304 $results = $this->zdb->execute($select);
305
306 if ($results->count() == 0) {
307 Analog::log(
308 'No entries (' . $this->getType() . ') defined in database.',
309 Analog::INFO
310 );
311 } else {
312 $pk = $this->fpk;
313 $flabel = $this->flabel;
314 $fprio = $this->fthird;
315
316 foreach ($results as $r) {
317 $list[$r->$pk] = array(
318 'name' => _T($r->$flabel),
319 'extra' => $r->$fprio
320 );
321 }
322 }
323 return $list;
324 } catch (Throwable $e) {
325 Analog::log(
326 'Cannot list entries (' . $this->getType() .
327 ') | ' . $e->getMessage(),
328 Analog::WARNING
329 );
330 throw $e;
331 }
332 }
333
334 /**
335 * Get a entry
336 *
337 * @param integer $id Entry ID
338 *
339 * @return mixed|false Row if succeed ; false: no such id
340 */
341 public function get($id)
342 {
343 if (!is_numeric($id)) {
344 $this->errors[] = _T("ID must be an integer!");
345 return false;
346 }
347
348 try {
349 $select = $this->zdb->select($this->table);
350 $select->where([$this->fpk => $id]);
351
352 $results = $this->zdb->execute($select);
353 $result = $results->current();
354
355 if (!$result) {
356 $this->errors[] = _T("Label does not exist");
357 return false;
358 }
359
360 return $result;
361 } catch (Throwable $e) {
362 Analog::log(
363 __METHOD__ . ' | ' . $e->getMessage(),
364 Analog::WARNING
365 );
366 throw $e;
367 }
368 }
369
370 /**
371 * Get a label
372 *
373 * @param integer $id Id
374 * @param boolean $translated Do we want translated or original label?
375 * Defaults to true.
376 *
377 * @return string|int
378 */
379 public function getLabel($id, $translated = true)
380 {
381 $res = $this->get($id);
382 if ($res === false) {
383 //get() already logged
384 return self::ID_NOT_EXITS;
385 };
386 $field = $this->flabel;
387 return ($translated) ? _T($res->$field) : $res->$field;
388 }
389
390 /**
391 * Get an ID from a label
392 *
393 * @param string $label The label
394 *
395 * @return int|false Return id if it exists false otherwise
396 */
397 public function getIdByLabel($label)
398 {
399 try {
400 $pk = $this->fpk;
401 $select = $this->zdb->select($this->table);
402 $select->columns(array($pk))
403 ->where(array($this->flabel => $label));
404
405 $results = $this->zdb->execute($select);
406 $result = $results->current();
407 if ($result) {
408 return $result->$pk;
409 } else {
410 return false;
411 }
412 } catch (Throwable $e) {
413 Analog::log(
414 'Unable to retrieve ' . $this->getType() . ' from label `' .
415 $label . '` | ' . $e->getMessage(),
416 Analog::ERROR
417 );
418 throw $e;
419 }
420 }
421
422 /**
423 * Add a new entry
424 *
425 * @param string $label The label
426 * @param integer $extra Extra values (priority for statuses,
427 * extension for contributions types, ...)
428 *
429 * @return bool|integer -2 : label already exists
430 */
431 public function add($label, $extra)
432 {
433 // Avoid duplicates.
434 $label = strip_tags($label);
435 $ret = $this->getIdByLabel($label);
436
437 if ($ret !== false) {
438 Analog::log(
439 $this->getType() . ' with label `' . $label . '` already exists',
440 Analog::WARNING
441 );
442 return -2;
443 }
444
445 try {
446 $this->zdb->connection->beginTransaction();
447 $values = array(
448 $this->flabel => $label,
449 $this->fthird => $extra
450 );
451
452 $insert = $this->zdb->insert($this->table);
453 $insert->values($values);
454
455 $ret = $this->zdb->execute($insert);
456
457 if ($ret->count() > 0) {
458 Analog::log(
459 'New ' . $this->getType() . ' `' . $label .
460 '` added successfully.',
461 Analog::INFO
462 );
463
464 $this->id = $this->zdb->getLastGeneratedValue($this);
465
466 $this->addTranslation($label);
467 } else {
468 throw new \Exception('New ' . $this->getType() . ' not added.');
469 }
470 $this->zdb->connection->commit();
471 return true;
472 } catch (Throwable $e) {
473 $this->zdb->connection->rollBack();
474 Analog::log(
475 'Unable to add new entry `' . $label . '` | ' .
476 $e->getMessage(),
477 Analog::ERROR
478 );
479 throw $e;
480 }
481 }
482
483 /**
484 * Update in database.
485 *
486 * @param integer $id Entry ID
487 * @param string $label The label
488 * @param integer $extra Extra values (priority for statuses,
489 * extension for contributions types, ...)
490 *
491 * @return self::ID_NOT_EXITS|boolean
492 */
493 public function update($id, $label, $extra)
494 {
495 $label = strip_tags($label);
496 $ret = $this->get($id);
497 if (!$ret) {
498 /* get() already logged and set $this->error. */
499 return self::ID_NOT_EXITS;
500 }
501
502 try {
503 $oldlabel = $ret->{$this->flabel};
504 $this->zdb->connection->beginTransaction();
505 $values = array(
506 $this->flabel => $label,
507 $this->fthird => $extra
508 );
509
510 $update = $this->zdb->update($this->table);
511 $update->set($values);
512 $update->where([$this->fpk => $id]);
513
514 $this->zdb->execute($update);
515
516 if ($oldlabel != $label) {
517 $this->deleteTranslation($oldlabel);
518 $this->addTranslation($label);
519 }
520
521 Analog::log(
522 $this->getType() . ' #' . $id . ' updated successfully.',
523 Analog::INFO
524 );
525 $this->zdb->connection->commit();
526 return true;
527 } catch (Throwable $e) {
528 $this->zdb->connection->rollBack();
529 Analog::log(
530 'Unable to update ' . $this->getType() . ' #' . $id . ' | ' .
531 $e->getMessage(),
532 Analog::ERROR
533 );
534 throw $e;
535 }
536 }
537
538 /**
539 * Delete entry
540 *
541 * @param integer $id Entry ID
542 *
543 * @return self::ID_NOT_EXITS|boolean
544 */
545 public function delete($id)
546 {
547 $ret = $this->get($id);
548 if (!$ret) {
549 /* get() already logged */
550 return self::ID_NOT_EXITS;
551 }
552
553 if ($this->isUsed($id)) {
554 $this->errors[] = _T("Cannot delete this label: it's still used");
555 return false;
556 }
557
558 try {
559 $this->zdb->connection->beginTransaction();
560 $delete = $this->zdb->delete($this->table);
561 $delete->where([$this->fpk => $id]);
562
563 $this->zdb->execute($delete);
564 $this->deleteTranslation($ret->{$this->flabel});
565
566 Analog::log(
567 $this->getType() . ' ' . $id . ' deleted successfully.',
568 Analog::INFO
569 );
570
571 $this->zdb->connection->commit();
572 return true;
573 } catch (Throwable $e) {
574 $this->zdb->connection->rollBack();
575 Analog::log(
576 'Unable to delete ' . $this->getType() . ' ' . $id .
577 ' | ' . $e->getMessage(),
578 Analog::ERROR
579 );
580 throw $e;
581 }
582 }
583
584 /**
585 * Check if this entry is used.
586 *
587 * @param integer $id Entry ID
588 *
589 * @return boolean
590 */
591 public function isUsed($id)
592 {
593 try {
594 $select = $this->zdb->select($this->used);
595 $select->where([$this->fpk => $id]);
596
597 $results = $this->zdb->execute($select);
598 $result = $results->current();
599
600 if ($result !== null) {
601 return true;
602 } else {
603 return false;
604 }
605 } catch (Throwable $e) {
606 Analog::log(
607 'Unable to check if ' . $this->getType() . ' `' . $id .
608 '` is used. | ' . $e->getMessage(),
609 Analog::ERROR
610 );
611 //in case of error, we consider that it is used, to avoid errors
612 return true;
613 }
614 }
615
616 /**
617 * Get textual type representation
618 *
619 * @return string
620 */
621 abstract protected function getType();
622
623 /**
624 * Get translated textual representation
625 *
626 * @return string
627 */
628 abstract public function getI18nType();
629
630 /**
631 * Global getter method
632 *
633 * @param string $name name of the property we want to retrive
634 *
635 * @return mixed the called property
636 */
637 public function __get($name)
638 {
639 $forbidden = array();
640 $virtuals = array('extension', 'libelle');
641 if (
642 in_array($name, $virtuals)
643 || !in_array($name, $forbidden)
644 && isset($this->$name)
645 ) {
646 switch ($name) {
647 case 'libelle':
648 return _T($this->label);
649 case 'extension':
650 return $this->third;
651 default:
652 return $this->$name;
653 }
654 } else {
655 return false;
656 }
657 }
658
659 /**
660 * Global isset method
661 * Required for twig to access properties via __get
662 *
663 * @param string $name name of the property we want to retrive
664 *
665 * @return bool
666 */
667 public function __isset($name)
668 {
669 $forbidden = array();
670 $virtuals = array('extension', 'libelle');
671 if (
672 in_array($name, $virtuals)
673 || !in_array($name, $forbidden)
674 && isset($this->$name)
675 ) {
676 return true;
677 }
678
679 return false;
680 }
681
682 /**
683 * Get errors
684 *
685 * @return array
686 */
687 public function getErrors(): array
688 {
689 return $this->errors;
690 }
691 }