]> git.agnieray.net Git - galette.git/blob - galette/lib/Galette/Entity/Group.php
Add and use isForeignKeyException, fix isDuplicateException
[galette.git] / galette / lib / Galette / Entity / Group.php
1 <?php
2
3 /* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4: */
4
5 /**
6 * Group entity
7 *
8 * PHP version 5
9 *
10 * Copyright © 2012-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 2012-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 - 2012-01-17
35 */
36
37 namespace Galette\Entity;
38
39 use ArrayObject;
40 use Galette\Repository\Groups;
41 use Throwable;
42 use Galette\Core\Login;
43 use Analog\Analog;
44 use Laminas\Db\Sql\Expression;
45
46 /**
47 * Group entity
48 *
49 * @category Entity
50 * @name Group
51 * @package Galette
52 * @author Johan Cwiklinski <johan@x-tnd.be>
53 * @copyright 2012-2023 The Galette Team
54 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
55 * @link http://galette.tuxfamily.org
56 * @since Available since 0.7dev - 2012-01-17
57 */
58 class Group
59 {
60 public const TABLE = 'groups';
61 public const PK = 'id_group';
62 //relations tables
63 public const GROUPSUSERS_TABLE = 'groups_members';
64 public const GROUPSMANAGERS_TABLE = 'groups_managers';
65
66 public const MEMBER_TYPE = 0;
67 public const MANAGER_TYPE = 1;
68
69 private $id;
70 private $group_name;
71 private $parent_group;
72 private $managers;
73 private $members;
74 private $groups;
75 private $creation_date;
76 private $count_members;
77 private $isempty;
78 private $login;
79
80 /**
81 * Default constructor
82 *
83 * @param null|int|ArrayObject $args Either a ResultSet row or its id for to load
84 * a specific group, or null to just
85 * instanciate object
86 */
87 public function __construct($args = null)
88 {
89 if ($args === null || is_int($args)) {
90 if (is_int($args) && $args > 0) {
91 $this->load($args);
92 }
93 } elseif (is_object($args)) {
94 $this->loadFromRS($args);
95 }
96 }
97
98 /**
99 * Loads a group from its id
100 *
101 * @param int $id the identifiant for the group to load
102 *
103 * @return bool true if query succeed, false otherwise
104 */
105 public function load($id)
106 {
107 global $zdb;
108
109 try {
110 $select = $zdb->select(self::TABLE);
111 $select->where(array(self::PK => $id));
112
113 $results = $zdb->execute($select);
114
115 if ($results->count() > 0) {
116 $this->loadFromRS($results->current());
117 return true;
118 } else {
119 return false;
120 }
121 } catch (Throwable $e) {
122 Analog::log(
123 'Cannot load group form id `' . $id . '` | ' . $e->getMessage(),
124 Analog::WARNING
125 );
126 throw $e;
127 }
128 }
129
130 /**
131 * Populate object from a resultset row
132 *
133 * @param ArrayObject $r the resultset row
134 *
135 * @return void
136 */
137 private function loadFromRS(ArrayObject $r)
138 {
139 $this->id = (int)$r->id_group;
140 $this->group_name = $r->group_name;
141 $this->creation_date = $r->creation_date;
142 if ($r->parent_group) {
143 $this->parent_group = new Group((int)$r->parent_group);
144 }
145 if (isset($r->members)) {
146 //we're from a list, we just want members count
147 $this->count_members = $r->members;
148 }
149 }
150
151 /**
152 * Loads members for the current group
153 *
154 * @param int $type Either self::MEMBER_TYPE or self::MANAGER_TYPE
155 *
156 * @return void
157 */
158 private function loadPersons($type)
159 {
160 global $zdb;
161
162 if ($this->id) {
163 try {
164 $join = null;
165 switch ($type) {
166 case self::MEMBER_TYPE:
167 $join = PREFIX_DB . self::GROUPSUSERS_TABLE;
168 break;
169 case self::MANAGER_TYPE:
170 $join = PREFIX_DB . self::GROUPSMANAGERS_TABLE;
171 break;
172 }
173
174 $select = $zdb->select(Adherent::TABLE, 'a');
175 $select->join(
176 array('status' => PREFIX_DB . Status::TABLE),
177 'a.' . Status::PK . '=status.' . Status::PK,
178 array('priorite_statut')
179 );
180 $select->join(
181 array('g' => $join),
182 'g.' . Adherent::PK . '=a.' . Adherent::PK,
183 array()
184 )->where([
185 'g.' . self::PK => $this->id
186 ])->order(
187 'nom_adh ASC',
188 'prenom_adh ASC'
189 );
190
191 $results = $zdb->execute($select);
192 $members = array();
193
194 $deps = array(
195 'picture' => false,
196 'groups' => false,
197 'dues' => false
198 );
199
200 foreach ($results as $m) {
201 $members[] = new Adherent($zdb, $m, $deps);
202 }
203
204 if ($type === self::MEMBER_TYPE) {
205 $this->members = $members;
206 } else {
207 $this->managers = $members;
208 }
209 } catch (Throwable $e) {
210 Analog::log(
211 'Cannot get group persons | ' . $e->getMessage(),
212 Analog::WARNING
213 );
214 throw $e;
215 }
216 }
217 }
218
219 /**
220 * Load sub-groups
221 *
222 * @return void
223 */
224 private function loadSubGroups()
225 {
226 global $zdb;
227
228 if (!isset($this->login) || !$this->login->isLogged()) {
229 $this->groups = [];
230 return;
231 }
232
233 try {
234 $select = $zdb->select(self::TABLE, 'a');
235
236 if (!$this->login->isAdmin() && !$this->login->isStaff()) {
237 $select->join(
238 array('b' => PREFIX_DB . self::GROUPSMANAGERS_TABLE),
239 'a.' . self::PK . '=b.' . self::PK,
240 array()
241 )->where(['b.' . Adherent::PK => $this->login->id]);
242 }
243
244 $select->where(['parent_group' => $this->id])
245 ->order('group_name ASC');
246
247 $results = $zdb->execute($select);
248 $groups = array();
249 $grppk = self::PK;
250 foreach ($results as $m) {
251 $group = new Group((int)$m->$grppk);
252 $group->setLogin($this->login);
253 $groups[] = $group;
254 }
255 $this->groups = $groups;
256 } catch (Throwable $e) {
257 Analog::log(
258 'Cannot get subgroup for group ' . $this->group_name .
259 ' (' . $this->id . ')| ' . $e->getMessage(),
260 Analog::WARNING
261 );
262 throw $e;
263 }
264 }
265
266 /**
267 * Remove specified group
268 *
269 * @param boolean $cascade Also remove members and managers
270 *
271 * @return boolean
272 */
273 public function remove($cascade = false)
274 {
275 global $zdb;
276 $transaction = false;
277
278 try {
279 if (!$zdb->connection->inTransaction()) {
280 $zdb->connection->beginTransaction();
281 $transaction = true;
282 }
283
284 if ($cascade === true) {
285 $subgroups = $this->getGroups();
286 if (count($subgroups) > 0) {
287 Analog::log(
288 'Cascading remove ' . $this->group_name .
289 '. Subgroups, their members and managers will be detached.',
290 Analog::INFO
291 );
292 foreach ($subgroups as $subgroup) {
293 $subgroup->remove(true);
294 }
295 }
296
297 Analog::log(
298 'Cascading remove ' . $this->group_name .
299 '. Members and managers will be detached.',
300 Analog::INFO
301 );
302
303 //delete members
304 $delete = $zdb->delete(self::GROUPSUSERS_TABLE);
305 $delete->where([self::PK => $this->id]);
306 $zdb->execute($delete);
307
308 //delete managers
309 $delete = $zdb->delete(self::GROUPSMANAGERS_TABLE);
310 $delete->where([self::PK => $this->id]);
311 $zdb->execute($delete);
312 }
313
314 //delete group itself
315 $delete = $zdb->delete(self::TABLE);
316 $delete->where([self::PK => $this->id]);
317 $zdb->execute($delete);
318
319 //commit all changes
320 if ($transaction) {
321 $zdb->connection->commit();
322 }
323
324 return true;
325 } catch (Throwable $e) {
326 if ($transaction) {
327 $zdb->connection->rollBack();
328 }
329 if ($zdb->isForeignKeyException($e)) {
330 Analog::log(
331 str_replace(
332 '%group',
333 $this->group_name,
334 'Group "%group" still have members!'
335 ),
336 Analog::WARNING
337 );
338 $this->isempty = false;
339 } else {
340 Analog::log(
341 'Unable to delete group ' . $this->group_name .
342 ' (' . $this->id . ') |' . $e->getMessage(),
343 Analog::ERROR
344 );
345 throw $e;
346 }
347 return false;
348 }
349 }
350
351 /**
352 * Is group empty? (after first deletion try)
353 *
354 * @return boolean
355 */
356 public function isEmpty()
357 {
358 return $this->isempty;
359 }
360
361 /**
362 * Detach a group from its parent
363 *
364 * @return boolean
365 */
366 public function detach()
367 {
368 global $zdb, $hist;
369
370 try {
371 $update = $zdb->update(self::TABLE);
372 $update->set(
373 array('parent_group' => new Expression('NULL'))
374 )->where(
375 [self::PK => $this->id]
376 );
377
378 $edit = $zdb->execute($update);
379
380 //edit == 0 does not mean there were an error, but that there
381 //were nothing to change
382 if ($edit->count() > 0) {
383 $this->parent_group = null;
384 $hist->add(
385 _T("Group has been detached from its parent"),
386 $this->group_name
387 );
388 }
389
390 return true;
391 } catch (Throwable $e) {
392 Analog::log(
393 'Something went wrong detaching group `' . $this->group_name .
394 '` (' . $this->id . ') from its parent:\'( | ' .
395 $e->getMessage() . "\n" .
396 $e->getTraceAsString(),
397 Analog::ERROR
398 );
399 throw $e;
400 }
401 }
402
403 /**
404 * Store the group
405 *
406 * @return boolean
407 */
408 public function store()
409 {
410 global $zdb, $hist;
411
412 $parent_group = null;
413 if ($this->parent_group) {
414 $parent_group = $this->parent_group->getId();
415 }
416 if (!Groups::isUnique($zdb, $this->getName(), $parent_group, $this->getId())) {
417 Analog::log(
418 'Group name is not unique at requested level',
419 Analog::WARNING
420 );
421 throw new \RuntimeException(
422 _T("The group name you have requested already exists in the database.")
423 );
424 }
425
426 try {
427 $values = array(
428 self::PK => $this->id,
429 'group_name' => $this->group_name
430 );
431
432 if ($this->parent_group) {
433 $values['parent_group'] = $parent_group;
434 }
435
436 if (!isset($this->id) || $this->id == '') {
437 //we're inserting a new group
438 unset($values[self::PK]);
439 $this->creation_date = date("Y-m-d H:i:s");
440 $values['creation_date'] = $this->creation_date;
441
442 $insert = $zdb->insert(self::TABLE);
443 $insert->values($values);
444 $add = $zdb->execute($insert);
445 if ($add->count() > 0) {
446 $this->id = $zdb->getLastGeneratedValue($this);
447
448 // logging
449 $hist->add(
450 _T("Group added"),
451 $this->group_name
452 );
453 return true;
454 } else {
455 $hist->add(_T("Fail to add new group."));
456 throw new \Exception(
457 'An error occurred inserting new group!'
458 );
459 }
460 } else {
461 //we're editing an existing group
462 $update = $zdb->update(self::TABLE);
463 $update
464 ->set($values)
465 ->where([self::PK => $this->id]);
466
467 $edit = $zdb->execute($update);
468
469 //edit == 0 does not mean there were an error, but that there
470 //were nothing to change
471 if ($edit->count() > 0) {
472 $hist->add(
473 _T("Group updated"),
474 $this->group_name
475 );
476 }
477 return true;
478 }
479 /** FIXME: also store members and managers? */
480 } catch (Throwable $e) {
481 Analog::log(
482 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
483 $e->getTraceAsString(),
484 Analog::ERROR
485 );
486 throw $e;
487 }
488 }
489
490 /**
491 * Is current logged-in user manager of the group?
492 *
493 * @param Login $login Login instance
494 *
495 * @return boolean
496 */
497 public function isManager(Login $login)
498 {
499 if ($login->isAdmin() || $login->isStaff()) {
500 //admins as well as staff members are managers for all groups!
501 return true;
502 } else {
503 //let's check if current logged-in user is part of group managers
504 if (!is_array($this->managers)) {
505 $this->loadPersons(self::MANAGER_TYPE);
506 }
507
508 foreach ($this->managers as $manager) {
509 if ($login->login == $manager->login) {
510 return true;
511 }
512 }
513 return false;
514 }
515 }
516
517 /**
518 * Get group id
519 *
520 * @return integer
521 */
522 public function getId()
523 {
524 return $this->id;
525 }
526
527 /**
528 * Get Level of the group
529 *
530 * @return integer
531 */
532 public function getLevel()
533 {
534 if ($this->parent_group) {
535 return $this->parent_group->getLevel() + 1;
536 }
537 return 0;
538 }
539
540 /**
541 * Get the full name of the group "foo / bar"
542 *
543 * @return string
544 */
545 public function getFullName()
546 {
547 if ($this->parent_group) {
548 return $this->parent_group->getFullName() . ' / ' . $this->group_name;
549 }
550 return $this->group_name;
551 }
552
553 /**
554 * Get the indented short name of the group " >> bar"
555 *
556 * @return string
557 */
558 public function getIndentName()
559 {
560 if (($level = $this->getLevel())) {
561 return str_repeat("&nbsp;", 3 * $level) . '&raquo; ' . $this->group_name;
562 }
563 return $this->group_name;
564 }
565
566 /**
567 * Get group name
568 *
569 * @return string
570 */
571 public function getName()
572 {
573 return $this->group_name;
574 }
575
576 /**
577 * Get group members
578 *
579 * @return Adherent[]
580 */
581 public function getMembers()
582 {
583 if (!is_array($this->members)) {
584 $this->loadPersons(self::MEMBER_TYPE);
585 }
586 return $this->members;
587 }
588
589 /**
590 * Get groups managers
591 *
592 * @return Adherent[]
593 */
594 public function getManagers()
595 {
596 if (!is_array($this->managers)) {
597 $this->loadPersons(self::MANAGER_TYPE);
598 }
599 return $this->managers;
600 }
601
602 /**
603 * Get subgroups
604 *
605 * @return Group[]
606 */
607 public function getGroups()
608 {
609 if (!is_array($this->groups)) {
610 $this->loadSubGroups();
611 }
612 return $this->groups;
613 }
614
615 /**
616 * Get parent group
617 *
618 * @return Group
619 */
620 public function getParentGroup()
621 {
622 return $this->parent_group;
623 }
624
625 /**
626 * Get group creation date
627 *
628 * @param boolean $formatted Return date formatted, raw if false
629 *
630 * @return string
631 */
632 public function getCreationDate($formatted = true)
633 {
634 if ($formatted === true) {
635 $date = new \DateTime($this->creation_date);
636 return $date->format(__("Y-m-d"));
637 } else {
638 return $this->creation_date;
639 }
640 }
641
642 /**
643 * Get member count
644 *
645 * @param boolean $force Force members load, defaults to false
646 *
647 * @return int
648 */
649 public function getMemberCount($force = false)
650 {
651 if (isset($this->members) && is_array($this->members)) {
652 return count($this->members);
653 } elseif (isset($this->count_members)) {
654 return $this->count_members;
655 } else {
656 if ($force === true) {
657 return count($this->getMembers());
658 } else {
659 return 0;
660 }
661 }
662 }
663
664 /**
665 * Set name
666 *
667 * @param string $name Group name
668 *
669 * @return Group
670 */
671 public function setName($name)
672 {
673 $this->group_name = $name;
674 return $this;
675 }
676
677 /**
678 * check if can Set parent group
679 *
680 * @param Group $group Parent group
681 *
682 * @return boolean
683 */
684 public function canSetParentGroup(Group $group)
685 {
686 do {
687 if ($group->getId() == $this->getId()) {
688 return false;
689 }
690 } while ($group = $group->getParentGroup());
691
692 //@phpstan-ignore-next-line
693 return true;
694 }
695
696 /**
697 * Set parent group
698 *
699 * @param int $id Parent group identifier
700 *
701 * @return Group
702 */
703 public function setParentGroup($id)
704 {
705 $group = new Group((int)$id);
706
707 if (!$this->canSetParentGroup($group)) {
708 //does not seem to work :/
709 throw new \Exception(
710 sprintf(
711 _T('Group `%1$s` cannot be set as parent!'),
712 $group->getName()
713 )
714 );
715 }
716
717 $this->parent_group = $group;
718 return $this;
719 }
720
721 /**
722 * Set members
723 *
724 * @param Adherent[] $members Members list
725 *
726 * @return bool
727 * @throws Throwable
728 */
729 public function setMembers(array $members = []): bool
730 {
731 global $zdb;
732
733 try {
734 $zdb->connection->beginTransaction();
735
736 //first, remove current groups members
737 $delete = $zdb->delete(self::GROUPSUSERS_TABLE);
738 $delete->where([self::PK => $this->id]);
739 $zdb->execute($delete);
740
741 Analog::log(
742 'Group members has been removed for `' . $this->group_name .
743 '`, we can now store new ones.',
744 Analog::INFO
745 );
746
747 $insert = $zdb->insert(self::GROUPSUSERS_TABLE);
748 $insert->values(
749 array(
750 self::PK => ':group',
751 Adherent::PK => ':adh'
752 )
753 );
754
755 $stmt = $zdb->sql->prepareStatementForSqlObject($insert);
756
757 foreach ($members as $m) {
758 $result = $stmt->execute(
759 array(
760 'group' => $this->id,
761 'adh' => $m->id
762 )
763 );
764
765 if ($result) {
766 Analog::log(
767 'Member `' . $m->sname . '` attached to group `' .
768 $this->group_name . '`.',
769 Analog::DEBUG
770 );
771 } else {
772 Analog::log(
773 'An error occurred trying to attach member `' .
774 $m->sname . '` to group `' . $this->group_name .
775 '` (' . $this->id . ').',
776 Analog::ERROR
777 );
778 throw new \Exception(
779 'Unable to attach `' . $m->sname . '` ' .
780 'to ' . $this->group_name . '(' . $this->id . ')'
781 );
782 }
783 }
784
785 //commit all changes
786 $zdb->connection->commit();
787
788 Analog::log(
789 'Group members updated successfully.',
790 Analog::INFO
791 );
792
793 return true;
794 } catch (Throwable $e) {
795 $te = clone $e;
796 $zdb->connection->rollBack();
797 $messages = array();
798 do {
799 $messages[] = $e->getMessage();
800 } while ($e = $e->getPrevious());
801 Analog::log(
802 'Unable to attach members to group `' . $this->group_name .
803 '` (' . $this->id . ')|' . implode("\n", $messages),
804 Analog::ERROR
805 );
806 throw $te;
807 }
808 }
809
810 /**
811 * Set managers
812 *
813 * @param Adherent[] $members Managers list
814 *
815 * @return bool
816 * @throws Throwable
817 */
818 public function setManagers(array $members = []): bool
819 {
820 global $zdb;
821
822 try {
823 $zdb->connection->beginTransaction();
824
825 //first, remove current groups managers
826 $delete = $zdb->delete(self::GROUPSMANAGERS_TABLE);
827 $delete->where([self::PK => $this->id]);
828 $zdb->execute($delete);
829
830 Analog::log(
831 'Group managers has been removed for `' . $this->group_name .
832 '`, we can now store new ones.',
833 Analog::INFO
834 );
835
836 $insert = $zdb->insert(self::GROUPSMANAGERS_TABLE);
837 $insert->values(
838 array(
839 self::PK => ':group',
840 Adherent::PK => ':adh'
841 )
842 );
843
844 $stmt = $zdb->sql->prepareStatementForSqlObject($insert);
845
846 foreach ($members as $m) {
847 $result = $stmt->execute(
848 array(
849 'group' => $this->id,
850 'adh' => $m->id
851 )
852 );
853
854 if ($result) {
855 Analog::log(
856 'Manager `' . $m->sname . '` attached to group `' .
857 $this->group_name . '`.',
858 Analog::DEBUG
859 );
860 } else {
861 Analog::log(
862 'An error occurred trying to attach manager `' .
863 $m->sname . '` to group `' . $this->group_name .
864 '` (' . $this->id . ').',
865 Analog::ERROR
866 );
867 throw new \Exception(
868 'Unable to attach `' . $m->sname . '` ' .
869 'to ' . $this->group_name . '(' . $this->id . ')'
870 );
871 }
872 }
873
874 //commit all changes
875 $zdb->connection->commit();
876
877 Analog::log(
878 'Groups managers updated successfully.',
879 Analog::INFO
880 );
881
882 return true;
883 } catch (Throwable $e) {
884 $te = clone $e;
885 $zdb->connection->rollBack();
886 $messages = array();
887 do {
888 $messages[] = $e->getMessage();
889 } while ($e = $e->getPrevious());
890 Analog::log(
891 'Unable to attach managers to group `' . $this->group_name .
892 '` (' . $this->id . ')|' . implode("\n", $messages),
893 Analog::ERROR
894 );
895 throw $te;
896 }
897 }
898
899 /**
900 * Set login instance
901 *
902 * @param Login $login Login instance
903 *
904 * @return Group
905 */
906 public function setLogin(Login $login)
907 {
908 $this->login = $login;
909 return $this;
910 }
911
912 /**
913 * Can current logged-in user edit group
914 *
915 * @param Login $login Login instance
916 *
917 * @return boolean
918 */
919 public function canEdit(Login $login): bool
920 {
921 global $preferences;
922
923 //admin and staff users can edit
924 if ($login->isAdmin() || $login->isStaff()) {
925 return true;
926 }
927
928 //group managers can edit groups they manage when pref is on
929 if ($preferences->pref_bool_groupsmanagers_edit_groups && $this->isManager($login)) {
930 return true;
931 }
932
933 return false;
934 }
935 }