]> git.agnieray.net Git - galette.git/blobdiff - galette/lib/Galette/Entity/Adherent.php
Disable events from mass changes; closes #1733
[galette.git] / galette / lib / Galette / Entity / Adherent.php
index c0344ad00cb2d083b17a19cf9500625821f4fba5..e6d67b4d94282f039c845efa3ac25c9182bf9686 100644 (file)
@@ -7,7 +7,7 @@
  *
  * PHP version 5
  *
- * Copyright © 2009-2021 The Galette Team
+ * Copyright © 2009-2023 The Galette Team
  *
  * This file is part of Galette (http://galette.tuxfamily.org).
  *
@@ -28,7 +28,7 @@
  * @package   Galette
  *
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2009-2021 The Galette Team
+ * @copyright 2009-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.7dev - 2009-06-02
@@ -36,6 +36,9 @@
 
 namespace Galette\Entity;
 
+use ArrayObject;
+use Galette\Events\GaletteEvent;
+use Galette\Features\HasEvent;
 use Galette\Features\Socials;
 use Throwable;
 use Analog\Analog;
@@ -58,7 +61,7 @@ use Galette\Features\Dynamics;
  * @name      Adherent
  * @package   Galette
  * @author    Johan Cwiklinski <johan@x-tnd.be>
- * @copyright 2009-2021 The Galette Team
+ * @copyright 2009-2023 The Galette Team
  * @license   http://www.gnu.org/licenses/gpl-3.0.html GPL License 3.0 or (at your option) any later version
  * @link      http://galette.tuxfamily.org
  * @since     Available since 0.7dev - 02-06-2009
@@ -66,12 +69,12 @@ use Galette\Features\Dynamics;
  * @property integer $id
  * @property integer|Title $title Either a title id or an instance of Title
  * @property string $stitle Title label
- * @property string company_name
+ * @property string $company_name
  * @property string $name
- * @property string $surname
+ * @property ?string $surname
  * @property string $nickname
- * @property string $birthdate Localized birth date
- * @property string $rbirthdate Raw birth date
+ * @property string $birthdate Localized birthdate
+ * @property string $rbirthdate Raw birthdate
  * @property string $birth_place
  * @property integer $gender
  * @property string $sgender Gender label
@@ -80,7 +83,6 @@ use Galette\Features\Dynamics;
  * @property integer $status
  * @property string $sstatus Status label
  * @property string $address
- * @property string $address_continuation
  * @property string $zipcode
  * @property string $town
  * @property string $country
@@ -113,18 +115,19 @@ use Galette\Features\Dynamics;
  * @property string $sactive yes/no
  * @property string $sfullname
  * @property string $sname
- * @property string $saddress Concatened address and continuation
+ * @property string $saddress
  * @property string $contribstatus State of member contributions
- * @property string $days_remaining
+ * @property integer $days_remaining
  * @property-read integer $parent_id
  * @property Social $social Social networks/Contact
  * @property string $number Member number
- *
+ * @property-read bool $self_adh
  */
 class Adherent
 {
     use Dynamics;
     use Socials;
+    use HasEvent;
 
     public const TABLE = 'adherents';
     public const PK = 'id_adh';
@@ -156,7 +159,6 @@ class Adherent
     private $_status;
     //Contact information
     private $_address;
-    private $_address_continuation; /** TODO: remove */
     private $_zipcode;
     private $_town;
     private $_country;
@@ -168,7 +170,7 @@ class Adherent
     //Galette relative information
     private $_appears_in_list;
     private $_admin;
-    private $_staff;
+    private $_staff = false;
     private $_due_free;
     private $_login;
     private $_password;
@@ -180,10 +182,10 @@ class Adherent
     private $_picture;
     private $_oldness;
     private $_days_remaining;
-    private $_groups;
-    private $_managed_groups;
+    private $_groups = [];
+    private $_managed_groups = [];
     private $_parent;
-    private $_children;
+    private $_children = [];
     private $_duplicate = false;
     private $_socials;
     private $_number;
@@ -191,15 +193,6 @@ class Adherent
     private $_row_classes;
 
     private $_self_adh = false;
-    private $_deps = array(
-        'picture'   => true,
-        'groups'    => true,
-        'dues'      => true,
-        'parent'    => false,
-        'children'  => false,
-        'dynamics'  => false,
-        'socials'   => false
-    );
 
     private $zdb;
     private $preferences;
@@ -208,7 +201,6 @@ class Adherent
 
     private $parent_fields = [
         'adresse_adh',
-        'adresse2_adh',
         'cp_adh',
         'ville_adh',
         'email_adh'
@@ -221,11 +213,11 @@ class Adherent
     /**
      * Default constructor
      *
-     * @param Db          $zdb  Database instance
-     * @param mixed       $args Either a ResultSet row, its id or its
-     *                          login or its email for to load s specific
-     *                          member, or null to just instantiate object
-     * @param false|array $deps Dependencies configuration, see Adherent::$_deps
+     * @param Db               $zdb  Database instance
+     * @param mixed            $args Either a ResultSet row, its id or its
+     *                               login or its email for to load s specific
+     *                               member, or null to just instantiate object
+     * @param false|array|null $deps Dependencies configuration, see Adherent::$_deps
      */
     public function __construct(Db $zdb, $args = null, $deps = null)
     {
@@ -235,16 +227,10 @@ class Adherent
 
         if ($deps !== null) {
             if (is_array($deps)) {
-                $this->_deps = array_merge(
-                    $this->_deps,
-                    $deps
-                );
+                $this->setDeps($deps);
             } elseif ($deps === false) {
                 //no dependencies
-                $this->_deps = array_fill_keys(
-                    array_keys($this->_deps),
-                    false
-                );
+                $this->disableAllDeps();
             } else {
                 Analog::log(
                     '$deps should be an array, ' . gettype($deps) . ' given!',
@@ -253,6 +239,12 @@ class Adherent
             }
         }
 
+        $this
+            ->withAddEvent()
+            ->withEditEvent()
+            ->withoutDeleteEvent()
+            ->activateEvents();
+
         if ($args == null || is_int($args)) {
             if (is_int($args) && $args > 0) {
                 $this->load($args);
@@ -290,7 +282,7 @@ class Adherent
      *
      * @return bool true if query succeed, false otherwise
      */
-    public function load($id)
+    public function load(int $id): bool
     {
         try {
             $select = $this->zdb->select(self::TABLE, 'a');
@@ -307,7 +299,9 @@ class Adherent
                 return false;
             }
 
-            $this->loadFromRS($results->current());
+            /** @var ArrayObject $result */
+            $result = $results->current();
+            $this->loadFromRS($result);
             return true;
         } catch (Throwable $e) {
             Analog::log(
@@ -323,9 +317,9 @@ class Adherent
      *
      * @param string $login login for the member to load
      *
-     * @return bool true if query succeed, false otherwise
+     * @return boolean
      */
-    public function loadFromLoginOrMail($login)
+    public function loadFromLoginOrMail(string $login): bool
     {
         try {
             $select = $this->zdb->select(self::TABLE);
@@ -338,10 +332,12 @@ class Adherent
             }
 
             $results = $this->zdb->execute($select);
-            $result = $results->current();
-            if ($result) {
+            if ($results->count() > 0) {
+                /** @var ArrayObject $result */
+                $result = $results->current();
                 $this->loadFromRS($result);
             }
+            return true;
         } catch (Throwable $e) {
             Analog::log(
                 'Cannot load member form login `' . $login . '` | ' .
@@ -355,11 +351,11 @@ class Adherent
     /**
      * Populate object from a resultset row
      *
-     * @param ResultSet $r the resultset row
+     * @param ArrayObject $r the resultset row
      *
      * @return void
      */
-    private function loadFromRS($r)
+    private function loadFromRS(ArrayObject $r): void
     {
         $this->_self_adh = false;
         $this->_id = $r->id_adh;
@@ -378,12 +374,10 @@ class Adherent
         $this->_gender = (int)$r->sexe_adh;
         $this->_job = $r->prof_adh;
         $this->_language = $r->pref_lang;
-        $this->_active = ($r->activite_adh == 1) ? true : false;
+        $this->_active = $r->activite_adh == 1;
         $this->_status = (int)$r->id_statut;
         //Contact information
         $this->_address = $r->adresse_adh;
-        /** TODO: remove and merge with address */
-        $this->_address_continuation = $r->adresse2_adh;
         $this->_zipcode = $r->cp_adh;
         $this->_town = $r->ville_adh;
         $this->_country = $r->pays_adh;
@@ -393,15 +387,15 @@ class Adherent
         $this->_gnupgid = $r->gpgid;
         $this->_fingerprint = $r->fingerprint;
         //Galette relative information
-        $this->_appears_in_list = ($r->bool_display_info == 1) ? true : false;
-        $this->_admin = ($r->bool_admin_adh == 1) ? true : false;
+        $this->_appears_in_list = $r->bool_display_info == 1;
+        $this->_admin = $r->bool_admin_adh == 1;
         if (
             isset($r->priorite_statut)
             && $r->priorite_statut < Members::NON_STAFF_MEMBERS
         ) {
             $this->_staff = true;
         }
-        $this->_due_free = ($r->bool_exempt_adh == 1) ? true : false;
+        $this->_due_free = $r->bool_exempt_adh == 1;
         $this->_login = $r->login_adh;
         $this->_password = $r->mdp_adh;
         $this->_creation_date = $r->date_crea_adh;
@@ -452,9 +446,9 @@ class Adherent
      *
      * @return void
      */
-    private function loadParent()
+    private function loadParent(): void
     {
-        if (!$this->_parent instanceof Adherent) {
+        if ($this->_parent !== null && !$this->_parent instanceof Adherent) {
             $deps = array_fill_keys(array_keys($this->_deps), false);
             $this->_parent = new Adherent($this->zdb, (int)$this->_parent, $deps);
         }
@@ -465,7 +459,7 @@ class Adherent
      *
      * @return void
      */
-    private function loadChildren()
+    private function loadChildren(): void
     {
         $this->_children = array();
         try {
@@ -473,9 +467,7 @@ class Adherent
             $select = $this->zdb->select(self::TABLE);
             $select->columns(
                 array($id)
-            )->where(
-                'parent_id = ' . $this->_id
-            );
+            )->where(['parent_id' => $this->_id]);
 
             $results = $this->zdb->execute($select);
 
@@ -502,7 +494,7 @@ class Adherent
      *
      * @return void
      */
-    public function loadGroups()
+    public function loadGroups(): void
     {
         $this->_groups = Groups::loadGroups($this->_id);
         $this->_managed_groups = Groups::loadManagedGroups($this->_id);
@@ -513,7 +505,7 @@ class Adherent
      *
      * @return void
      */
-    public function loadSocials()
+    public function loadSocials(): void
     {
         $this->_socials = Social::getListForMember($this->_id);
     }
@@ -521,10 +513,10 @@ class Adherent
     /**
      * Retrieve status from preferences
      *
-     * @return pref_statut
+     * @return integer
      *
      */
-    private function getDefaultStatus()
+    private function getDefaultStatus(): int
     {
         global $preferences;
         if ($preferences->pref_statut != '') {
@@ -543,11 +535,11 @@ class Adherent
      *
      * @return void
      */
-    private function checkDues()
+    private function checkDues(): void
     {
         //how many days since our beloved member has been created
-        $date_now = new \DateTime();
-        $this->_oldness = $date_now->diff(
+        $now = new \DateTime();
+        $this->_oldness = $now->diff(
             new \DateTime($this->_creation_date)
         )->days;
 
@@ -559,21 +551,28 @@ class Adherent
             if ($this->_due_date == '') {
                 $this->_row_classes .= ' cotis-never';
             } else {
-                $date_end = new \DateTime($this->_due_date);
-                $date_diff = $date_now->diff($date_end);
-                $this->_days_remaining = ($date_diff->invert == 1)
-                    ? $date_diff->days * -1
-                    : $date_diff->days;
-
-                if ($this->_days_remaining == 0) {
-                    $this->_row_classes .= ' cotis-lastday';
-                } elseif ($this->_days_remaining < 0) {
+                // To count the days remaining, the next begin date is required.
+                $due_date = new \DateTime($this->_due_date);
+                $next_begin_date = clone $due_date;
+                $next_begin_date->add(new \DateInterval('P1D'));
+                $date_diff = $now->diff($next_begin_date);
+                $this->_days_remaining = $date_diff->days;
+                // Active
+                if ($date_diff->invert == 0 && $date_diff->days >= 0) {
+                    $this->_days_remaining = $date_diff->days;
+                    if ($this->_days_remaining <= 30) {
+                        if ($date_diff->days == 0) {
+                            $this->_row_classes .= ' cotis-lastday';
+                        }
+                        $this->_row_classes .= ' cotis-soon';
+                    } else {
+                        $this->_row_classes .= ' cotis-ok';
+                    }
+                // Expired
+                } elseif ($date_diff->invert == 1 && $date_diff->days >= 0) {
+                    $this->_days_remaining = $date_diff->days;
                     //check if member is still active
                     $this->_row_classes .= $this->isActive() ? ' cotis-late' : ' cotis-old';
-                } elseif ($this->_days_remaining < 30) {
-                    $this->_row_classes .= ' cotis-soon';
-                } else {
-                    $this->_row_classes .= ' cotis-ok';
                 }
             }
         }
@@ -584,7 +583,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isAdmin()
+    public function isAdmin(): bool
     {
         return $this->_admin;
     }
@@ -594,7 +593,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isStaff()
+    public function isStaff(): bool
     {
         return $this->_staff;
     }
@@ -604,7 +603,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isDueFree()
+    public function isDueFree(): bool
     {
         return $this->_due_free;
     }
@@ -616,22 +615,18 @@ class Adherent
      *
      * @return boolean
      */
-    public function isGroupMember($group_name)
+    public function isGroupMember(string $group_name): bool
     {
-        if (is_array($this->_groups)) {
-            foreach ($this->_groups as $g) {
-                if ($g->getName() == $group_name) {
-                    return true;
-                    break;
-                }
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
+
+        foreach ($this->_groups as $g) {
+            if ($g->getName() == $group_name) {
+                return true;
             }
-        } else {
-            Analog::log(
-                'Calling ' . __METHOD__ . ' without groups loaded!',
-                Analog::ERROR
-            );
-            return false;
         }
+        return false;
     }
 
     /**
@@ -641,22 +636,18 @@ class Adherent
      *
      * @return boolean
      */
-    public function isGroupManager($group_name)
+    public function isGroupManager(string $group_name): bool
     {
-        if (is_array($this->_managed_groups)) {
-            foreach ($this->_managed_groups as $mg) {
-                if ($mg->getName() == $group_name) {
-                    return true;
-                    break;
-                }
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
+
+        foreach ($this->_managed_groups as $mg) {
+            if ($mg->getName() == $group_name) {
+                return true;
             }
-        } else {
-            Analog::log(
-                'Calling ' . __METHOD__ . ' without groups loaded!',
-                Analog::ERROR
-            );
-            return false;
         }
+        return false;
     }
 
     /**
@@ -664,7 +655,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isCompany()
+    public function isCompany(): bool
     {
         return trim($this->_company_name ?? '') != '';
     }
@@ -674,7 +665,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isMan()
+    public function isMan(): bool
     {
         return (int)$this->_gender === self::MAN;
     }
@@ -684,7 +675,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isWoman()
+    public function isWoman(): bool
     {
         return (int)$this->_gender === self::WOMAN;
     }
@@ -695,7 +686,7 @@ class Adherent
      *
      * @return bool
      */
-    public function appearsInMembersList()
+    public function appearsInMembersList(): bool
     {
         return $this->_appears_in_list;
     }
@@ -705,7 +696,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isActive()
+    public function isActive(): bool
     {
         return $this->_active;
     }
@@ -715,7 +706,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasPicture()
+    public function hasPicture(): bool
     {
         return $this->_picture->hasPicture();
     }
@@ -725,7 +716,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasParent()
+    public function hasParent(): bool
     {
         return !empty($this->_parent);
     }
@@ -735,7 +726,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasChildren()
+    public function hasChildren(): bool
     {
         if ($this->_children === null) {
             if ($this->id) {
@@ -757,9 +748,9 @@ class Adherent
      *
      * @return string the class to apply
      */
-    public function getRowClass($public = false)
+    public function getRowClass(bool $public = false): string
     {
-        $strclass = ($this->isActive()) ? 'active' : 'inactive';
+        $strclass = ($this->isActive()) ? 'active-account' : 'inactive-account';
         if ($public === false) {
             $strclass .= $this->_row_classes;
         }
@@ -771,13 +762,23 @@ class Adherent
      *
      * @return string i18n string representing state of due
      */
-    public function getDues()
+    public function getDues(): string
     {
         $ret = '';
-        $date_now = new \DateTime();
+        $never_contributed = false;
+        $now = new \DateTime();
+        // To count the days remaining, the next begin date is required.
+        if ($this->_due_date === null) {
+            $this->_due_date = $now->format('Y-m-d');
+            $never_contributed = true;
+        }
+        $due_date = new \DateTime($this->_due_date);
+        $next_begin_date = clone $due_date;
+        $next_begin_date->add(new \DateInterval('P1D'));
+        $date_diff = $now->diff($next_begin_date);
         if ($this->isDueFree()) {
             $ret = _T("Freed of dues");
-        } elseif ($this->_due_date == '') {
+        } elseif ($never_contributed === true) {
             $patterns = array('/%days/', '/%date/');
             $cdate = new \DateTime($this->_creation_date);
             $replace = array(
@@ -793,20 +794,32 @@ class Adherent
             } else {
                 $ret = _T("Never contributed");
             }
+        // Last active or first expired day
         } elseif ($this->_days_remaining == 0) {
-            $ddate = new \DateTime($this->_due_date);
-            $date_diff = $date_now->diff($ddate);
             if ($date_diff->invert == 0) {
                 $ret = _T("Last day!");
             } else {
                 $ret = _T("Late since today!");
             }
-        } elseif ($this->_days_remaining < 0) {
-            $ddate = new \DateTime($this->_due_date);
+        // Active
+        } elseif ($date_diff->invert == 0 && $this->_days_remaining > 0) {
             $patterns = array('/%days/', '/%date/');
             $replace = array(
-                $this->_days_remaining * -1,
-                $ddate->format(__("Y-m-d"))
+                $this->_days_remaining,
+                $due_date->format(__("Y-m-d"))
+            );
+            $ret = preg_replace(
+                $patterns,
+                $replace,
+                _T("%days days remaining (ending on %date)")
+            );
+        // Expired
+        } elseif ($date_diff->invert == 1 && $this->_days_remaining > 0) {
+            $patterns = array('/%days/', '/%date/');
+            $replace = array(
+                // We need the number of days expired, not the number of days remaining.
+                $this->_days_remaining + 1,
+                $due_date->format(__("Y-m-d"))
             );
             if ($this->_active) {
                 $ret = preg_replace(
@@ -817,18 +830,6 @@ class Adherent
             } else {
                 $ret = _T("No longer member");
             }
-        } else {
-            $ddate = new \DateTime($this->_due_date);
-            $patterns = array('/%days/', '/%date/');
-            $replace = array(
-                $this->_days_remaining,
-                $ddate->format(__("Y-m-d"))
-            );
-            $ret = preg_replace(
-                $patterns,
-                $replace,
-                _T("%days days remaining (ending on %date)")
-            );
         }
         return $ret;
     }
@@ -843,11 +844,11 @@ class Adherent
      *
      * @return string formatted Name and Surname
      */
-    public static function getSName($zdb, $id, $wid = false, $wnick = false)
+    public static function getSName(Db $zdb, int $id, bool $wid = false, bool $wnick = false): string
     {
         try {
             $select = $zdb->select(self::TABLE);
-            $select->where(self::PK . ' = ' . $id);
+            $select->where([self::PK => $id]);
 
             $results = $zdb->execute($select);
             $row = $results->current();
@@ -872,37 +873,42 @@ class Adherent
      * Get member name with correct case
      *
      * @param string        $name    Member name
-     * @param string        $surname Mmeber surname
+     * @param string        $surname Member surname
      * @param false|Title   $title   Member title to show or false
      * @param false|integer $id      Member id to display or false
      * @param false|string  $nick    Member nickname to display or false
      *
      * @return string
      */
-    public static function getNameWithCase($name, $surname, $title = false, $id = false, $nick = false)
-    {
+    public static function getNameWithCase(
+        ?string $name,
+        ?string $surname,
+        $title = false,
+        $id = false,
+        $nick = false
+    ): string {
         $str = '';
 
-        if ($title !== false && $title instanceof Title) {
+        if ($title instanceof Title) {
             $str .= $title->tshort . ' ';
         }
 
         $str .= mb_strtoupper($name ?? '', 'UTF-8') . ' ' .
             ucwords(mb_strtolower($surname ?? '', 'UTF-8'), " \t\r\n\f\v-_|");
 
-        if ($id !== false || $nick !== false) {
+        if ($id !== false || !empty($nick)) {
             $str .= ' (';
         }
-        if ($nick !== false) {
+        if (!empty($nick)) {
             $str .= $nick;
         }
         if ($id !== false) {
-            if ($nick !== false && !empty($nick)) {
+            if (!empty($nick)) {
                 $str .= ', ';
             }
             $str .= $id;
         }
-        if ($id !== false || $nick !== false) {
+        if ($id !== false || !empty($nick)) {
             $str .= ')';
         }
         return strip_tags($str);
@@ -911,13 +917,13 @@ class Adherent
     /**
      * Change password for a given user
      *
-     * @param Db     $zdb    Database instance
-     * @param string $id_adh Member identifier
-     * @param string $pass   New password
+     * @param Db      $zdb    Database instance
+     * @param integer $id_adh Member identifier
+     * @param string  $pass   New password
      *
      * @return boolean
      */
-    public static function updatePassword(Db $zdb, $id_adh, $pass)
+    public static function updatePassword(Db $zdb, int $id_adh, string $pass): bool
     {
         try {
             $cpass = password_hash($pass, PASSWORD_BCRYPT);
@@ -925,7 +931,7 @@ class Adherent
             $update = $zdb->update(self::TABLE);
             $update->set(
                 array('mdp_adh' => $cpass)
-            )->where(self::PK . ' = ' . $id_adh);
+            )->where([self::PK => $id_adh]);
             $zdb->execute($update);
             Analog::log(
                 'Password for `' . $id_adh . '` has been updated.',
@@ -949,13 +955,13 @@ class Adherent
      *
      * @return string
      */
-    private function getFieldLabel($field)
+    private function getFieldLabel(string $field): string
     {
-        $label = $this->fields[$field]['label'];
+        $label = $this->fields[$field]['label'] ?? '';
         //replace "&nbsp;"
         $label = str_replace('&nbsp;', ' ', $label);
         //remove trailing ':' and then trim
-        $label = trim(trim($label ?? '', ':'));
+        $label = trim(trim($label, ':'));
         return $label;
     }
 
@@ -966,7 +972,7 @@ class Adherent
      *
      * @return array
      */
-    public static function getDbFields(Db $zdb)
+    public static function getDbFields(Db $zdb): array
     {
         $columns = $zdb->getColumns(self::TABLE);
         $fields = array();
@@ -981,7 +987,7 @@ class Adherent
      *
      * @return void
      */
-    public function setSelfMembership()
+    public function setSelfMembership(): void
     {
         $this->_self_adh = true;
     }
@@ -991,27 +997,25 @@ class Adherent
      *
      * @return boolean
      */
-    public function isUp2Date()
+    public function isUp2Date(): bool
     {
-        if ($this->_deps['dues']) {
-            if ($this->isDueFree()) {
-                //member is due free, he's up to date.
-                return true;
+        if (!$this->isDepEnabled('dues')) {
+            $this->checkDues();
+        }
+
+        if ($this->isDueFree()) {
+            //member is due free, he's up to date.
+            return true;
+        } else {
+            //let's check from due date, if present
+            if ($this->_due_date == null) {
+                return false;
             } else {
-                //let's check from end date, if present
-                if ($this->_due_date == null) {
-                    return false;
-                } else {
-                    $ech = new \DateTime($this->_due_date);
-                    $now = new \DateTime();
-                    $now->setTime(0, 0, 0);
-                    return $ech >= $now;
-                }
+                $due_date = new \DateTime($this->_due_date);
+                $now = new \DateTime();
+                $now->setTime(0, 0, 0);
+                return $due_date >= $now;
             }
-        } else {
-            throw new \RuntimeException(
-                'Cannot check if member is up to date, dues deps is disabled!'
-            );
         }
     }
 
@@ -1044,8 +1048,10 @@ class Adherent
      *
      * @return true|array
      */
-    public function check($values, $required, $disabled)
+    public function check(array $values, array $required, array $disabled)
     {
+        global $login;
+
         $this->errors = array();
 
         //Sanitize
@@ -1085,6 +1091,7 @@ class Adherent
             if (isset($values[$key])) {
                 $value = $values[$key];
                 if ($value !== true && $value !== false) {
+                    //@phpstan-ignore-next-line
                     $value = trim($value ?? '');
                 }
             } elseif (empty($this->_id)) {
@@ -1184,6 +1191,23 @@ class Adherent
             $this->_parent = null;
         }
 
+        if ($login->isGroupManager() && !$login->isAdmin() && !$login->isStaff() && $this->parent_id !== $login->id) {
+            if (!isset($values['groups_adh'])) {
+                $this->errors[] = _T('You have to select a group you own!');
+            } else {
+                $owned_group = false;
+                foreach ($values['groups_adh'] as $group) {
+                    list($gid) = explode('|', $group);
+                    if ($login->isGroupManager($gid)) {
+                        $owned_group = true;
+                    }
+                }
+                if ($owned_group === false) {
+                    $this->errors[] = _T('You have to select a group you own!');
+                }
+            }
+        }
+
         $this->dynamicsCheck($values, $required, $disabled);
         $this->checkSocials($values);
 
@@ -1215,7 +1239,7 @@ class Adherent
      *
      * @return void
      */
-    public function validate($field, $value, $values)
+    public function validate(string $field, $value, array $values): void
     {
         global $preferences;
 
@@ -1300,29 +1324,29 @@ class Adherent
                     $this->errors[] = _T("- Non-valid E-Mail address!") .
                         ' (' . $this->getFieldLabel($field) . ')';
                 }
-                if ($field == 'email_adh') {
-                    try {
-                        $select = $this->zdb->select(self::TABLE);
-                        $select->columns(
-                            array(self::PK)
-                        )->where(array('email_adh' => $value));
-                        if (!empty($this->_id)) {
-                            $select->where(
-                                self::PK . ' != ' . $this->_id
-                            );
-                        }
 
-                        $results = $this->zdb->execute($select);
-                        if ($results->count() !== 0) {
-                            $this->errors[] = _T("- This E-Mail address is already used by another member!");
-                        }
-                    } catch (Throwable $e) {
-                        Analog::log(
-                            'An error occurred checking member email unicity.',
-                            Analog::ERROR
+                try {
+                    $select = $this->zdb->select(self::TABLE);
+                    $select->columns(
+                        array(self::PK)
+                    )->where(array('email_adh' => $value));
+                    if (!empty($this->_id)) {
+                        $select->where->notEqualTo(
+                            self::PK,
+                            $this->_id
                         );
-                        $this->errors[] = _T("An error has occurred while looking if login already exists.");
                     }
+
+                    $results = $this->zdb->execute($select);
+                    if ($results->count() !== 0) {
+                        $this->errors[] = _T("- This E-Mail address is already used by another member!");
+                    }
+                } catch (Throwable $e) {
+                    Analog::log(
+                        'An error occurred checking member email uniqueness.',
+                        Analog::ERROR
+                    );
+                    $this->errors[] = _T("An error has occurred while looking if login already exists.");
                 }
                 break;
             case 'login_adh':
@@ -1345,8 +1369,9 @@ class Adherent
                                 array(self::PK)
                             )->where(array('login_adh' => $value));
                             if (!empty($this->_id)) {
-                                $select->where(
-                                    self::PK . ' != ' . $this->_id
+                                $select->where->notEqualTo(
+                                    self::PK,
+                                    $this->_id
                                 );
                             }
 
@@ -1404,7 +1429,7 @@ class Adherent
                     $this->$prop = (int)$value;
                     //check if status exists
                     $select = $this->zdb->select(Status::TABLE);
-                    $select->where(Status::PK . '= ' . $value);
+                    $select->where([Status::PK => $value]);
 
                     $results = $this->zdb->execute($select);
                     $result = $results->current();
@@ -1443,7 +1468,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function store()
+    public function store(): bool
     {
         global $hist, $emitter, $login;
         $event = null;
@@ -1525,7 +1550,6 @@ class Adherent
                 }
             }
 
-            $success = false;
             if (empty($this->_id)) {
                 //we're inserting a new member
                 unset($values[self::PK]);
@@ -1552,9 +1576,8 @@ class Adherent
                             $this->sname
                         );
                     }
-                    $success = true;
 
-                    $event = 'member.add';
+                    $event = $this->getAddEventName();
                 } else {
                     $hist->add(_T("Fail to add new member."));
                     throw new \Exception(
@@ -1577,9 +1600,7 @@ class Adherent
 
                 $update = $this->zdb->update(self::TABLE);
                 $update->set($values);
-                $update->where(
-                    self::PK . '=' . $this->_id
-                );
+                $update->where([self::PK => $this->_id]);
 
                 $edit = $this->zdb->execute($update);
 
@@ -1592,21 +1613,18 @@ class Adherent
                         $this->sname
                     );
                 }
-                $success = true;
-                $event = 'member.edit';
+                $event = $this->getEditEventName();
             }
 
             //dynamic fields
-            if ($success) {
-                $success = $this->dynamicsStore();
-                $this->storeSocials($this->id);
-            }
+            $this->dynamicsStore();
+            $this->storeSocials($this->id);
 
             //send event at the end of process, once all has been stored
-            if ($event !== null) {
-                $emitter->emit($event, $this);
+            if ($event !== null && $this->areEventsEnabled()) {
+                $emitter->dispatch(new GaletteEvent($event, $this));
             }
-            return $success;
+            return true;
         } catch (Throwable $e) {
             Analog::log(
                 'Something went wrong :\'( | ' . $e->getMessage() . "\n" .
@@ -1622,16 +1640,16 @@ class Adherent
      *
      * @return void
      */
-    private function updateModificationDate()
+    private function updateModificationDate(): void
     {
         try {
             $modif_date = date('Y-m-d');
             $update = $this->zdb->update(self::TABLE);
             $update->set(
                 array('date_modif_adh' => $modif_date)
-            )->where(self::PK . '=' . $this->_id);
+            )->where([self::PK => $this->_id]);
 
-            $edit = $this->zdb->execute($update);
+            $this->zdb->execute($update);
             $this->_modification_date = $modif_date;
         } catch (Throwable $e) {
             Analog::log(
@@ -1648,13 +1666,13 @@ class Adherent
      *
      * @param string $name name of the property we want to retrieve
      *
-     * @return false|object the called property
+     * @return mixed
      */
-    public function __get($name)
+    public function __get(string $name)
     {
         $forbidden = array(
             'admin', 'staff', 'due_free', 'appears_in_list', 'active',
-            'row_classes', 'oldness', 'duplicate'
+            'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
         );
         if (!defined('GALETTE_TESTS')) {
             $forbidden[] = 'password'; //keep that for tests only
@@ -1669,6 +1687,10 @@ class Adherent
         $socials = array('website', 'msn', 'jabber', 'icq');
 
         if (in_array($name, $forbidden)) {
+            Analog::log(
+                'Calling property "' . $name . '" directly is discouraged.',
+                Analog::WARNING
+            );
             switch ($name) {
                 case 'admin':
                     return $this->isAdmin();
@@ -1682,6 +1704,10 @@ class Adherent
                     return $this->isActive();
                 case 'duplicate':
                     return $this->isDuplicate();
+                case 'groups':
+                    return $this->getGroups();
+                case 'managed_groups':
+                    return $this->getManagedGroups();
                 default:
                     throw new \RuntimeException("Call to __get for '$name' is forbidden!");
             }
@@ -1695,45 +1721,37 @@ class Adherent
             }
             switch ($name) {
                 case 'sadmin':
+                    return (($this->isAdmin()) ? _T("Yes") : _T("No"));
                 case 'sdue_free':
+                    return (($this->isDueFree()) ? _T("Yes") : _T("No"));
                 case 'sappears_in_list':
+                    return (($this->appearsInMembersList()) ? _T("Yes") : _T("No"));
                 case 'sstaff':
                     return (($this->$real) ? _T("Yes") : _T("No"));
-                    break;
                 case 'sactive':
-                    return (($this->$real) ? _T("Active") : _T("Inactive"));
-                    break;
+                    return (($this->isActive()) ? _T("Active") : _T("Inactive"));
                 case 'stitle':
                     if (isset($this->_title) && $this->_title instanceof Title) {
                         return $this->_title->tshort;
                     } else {
                         return null;
                     }
-                    break;
                 case 'sstatus':
                     $status = new Status($this->zdb);
                     return $status->getLabel($this->_status);
-                    break;
                 case 'sfullname':
                     return $this->getNameWithCase(
                         $this->_name,
                         $this->_surname,
                         (isset($this->_title) ? $this->title : false)
                     );
-                    break;
                 case 'saddress':
                     $address = $this->_address;
-                    if ($this->_address_continuation !== '' && $this->_address_continuation !== null) {
-                        $address .= "\n" . $this->_address_continuation;
-                    }
                     return $address;
-                    break;
                 case 'sname':
                     return $this->getNameWithCase($this->_name, $this->_surname);
-                    break;
                 case 'rbirthdate':
                     return $this->_birthdate;
-                    break;
                 case 'sgender':
                     switch ($this->gender) {
                         case self::MAN:
@@ -1743,10 +1761,8 @@ class Adherent
                         default:
                             return _T('Unspecified');
                     }
-                    break;
                 case 'contribstatus':
                     return $this->getDues();
-                    break;
             }
         }
 
@@ -1770,11 +1786,8 @@ class Adherent
                 } else {
                     return null;
                 }
-                break;
             case 'address':
-            case 'address_continuation':
                 return $this->$rname ?? '';
-                break;
             case 'birthdate':
             case 'creation_date':
             case 'modification_date':
@@ -1796,7 +1809,6 @@ class Adherent
                 break;
             case 'parent_id':
                 return ($this->_parent instanceof Adherent) ? (int)$this->_parent->id : (int)$this->_parent;
-                break;
             default:
                 if (!property_exists($this, $rname)) {
                     Analog::log(
@@ -1807,61 +1819,118 @@ class Adherent
                 } else {
                     return $this->$rname;
                 }
-                break;
         }
     }
 
     /**
-     * Get member email
-     * If member does not have an email address, but is attached to
-     * another member, we'll take information from its parent.
+     * Global isset method
+     * Required for twig to access properties via __get
      *
-     * @return string
+     * @param string $name name of the property we want to retrieve
+     *
+     * @return bool
      */
-    public function getEmail()
+    public function __isset(string $name)
     {
-        $email = $this->_email;
-        if (empty($email)) {
-            $this->loadParent();
-            $email = $this->parent->email;
+        $forbidden = array(
+            'admin', 'staff', 'due_free', 'appears_in_list', 'active',
+            'row_classes', 'oldness', 'duplicate', 'groups', 'managed_groups'
+        );
+        if (!defined('GALETTE_TESTS')) {
+            $forbidden[] = 'password'; //keep that for tests only
+        }
+
+        $virtuals = array(
+            'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
+            'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
+            'rbirthdate', 'sgender', 'contribstatus',
+        );
+
+        $socials = array('website', 'msn', 'jabber', 'icq');
+
+        if (in_array($name, $forbidden)) {
+            Analog::log(
+                'Calling property "' . $name . '" directly is discouraged.',
+                Analog::WARNING
+            );
+            switch ($name) {
+                case 'admin':
+                case 'staff':
+                case 'due_free':
+                case 'appears_in_list':
+                case 'active':
+                case 'duplicate':
+                case 'groups':
+                case 'managed_groups':
+                    return true;
+            }
+
+            return false;
+        }
+
+        if (in_array($name, $virtuals)) {
+            return true;
+        }
+
+        //for backward compatibility
+        if (in_array($name, $socials)) {
+            return true;
+        }
+
+        if (substr($name, 0, 1) !== '_') {
+            $rname = '_' . $name;
+        } else {
+            $rname = $name;
         }
 
-        return $email;
+        switch ($name) {
+            case 'id':
+            case 'id_statut':
+            case 'address':
+            case 'birthdate':
+            case 'creation_date':
+            case 'modification_date':
+            case 'due_date':
+            case 'parent_id':
+                return true;
+            default:
+                return property_exists($this, $rname);
+        }
     }
 
     /**
-     * Get member address.
-     * If member does not have an address, but is attached to another member, we'll take information from its parent.
+     * Get member email
+     * If member does not have an email address, but is attached to
+     * another member, we'll take information from its parent.
      *
      * @return string
      */
-    public function getAddress()
+    public function getEmail(): string
     {
-        $address = $this->_address;
-        if (empty($address) && $this->hasParent()) {
+        $email = $this->_email;
+        if (empty($email) && $this->hasParent()) {
             $this->loadParent();
-            $address = $this->parent->address;
+            $email = $this->parent->email;
         }
 
-        return $address;
+        return $email ?? '';
     }
 
     /**
-     * Get member address continuation.
+     * Get member address.
      * If member does not have an address, but is attached to another member, we'll take information from its parent.
      *
      * @return string
      */
-    public function getAddressContinuation()
+    public function getAddress(): string
     {
         $address = $this->_address;
-        $address_continuation = $this->_address_continuation;
         if (empty($address) && $this->hasParent()) {
             $this->loadParent();
-            $address_continuation = $this->parent->address_continuation;
+            $address = $this->parent->address;
         }
 
-        return $address_continuation;
+        return $address ?? '';
     }
 
     /**
@@ -1870,7 +1939,7 @@ class Adherent
      *
      * @return string
      */
-    public function getZipcode()
+    public function getZipcode(): string
     {
         $address = $this->_address;
         $zip = $this->_zipcode;
@@ -1879,7 +1948,7 @@ class Adherent
             $zip = $this->parent->zipcode;
         }
 
-        return $zip;
+        return $zip ?? '';
     }
 
     /**
@@ -1888,7 +1957,7 @@ class Adherent
      *
      * @return string
      */
-    public function getTown()
+    public function getTown(): string
     {
         $address = $this->_address;
         $town = $this->_town;
@@ -1897,7 +1966,7 @@ class Adherent
             $town = $this->parent->town;
         }
 
-        return $town;
+        return $town ?? '';
     }
 
     /**
@@ -1906,7 +1975,7 @@ class Adherent
      *
      * @return string
      */
-    public function getCountry()
+    public function getCountry(): string
     {
         $address = $this->_address;
         $country = $this->_country;
@@ -1915,7 +1984,7 @@ class Adherent
             $country = $this->parent->country;
         }
 
-        return $country;
+        return $country ?? '';
     }
 
     /**
@@ -1923,7 +1992,7 @@ class Adherent
      *
      * @return string
      */
-    public function getAge()
+    public function getAge(): string
     {
         if ($this->_birthdate == null) {
             return '';
@@ -1935,7 +2004,7 @@ class Adherent
                 'Invalid birthdate: ' . $this->_birthdate,
                 Analog::ERROR
             );
-            return;
+            return '';
         }
 
         return str_replace(
@@ -1950,7 +2019,7 @@ class Adherent
      *
      * @return array
      */
-    public function getParentFields()
+    public function getParentFields(): array
     {
         return $this->parent_fields;
     }
@@ -1958,11 +2027,12 @@ class Adherent
     /**
      * Handle files (photo and dynamics files)
      *
-     * @param array $files Files sent
+     * @param array $files    Files sent
+     * @param array $cropping Cropping properties
      *
      * @return array|true
      */
-    public function handleFiles($files)
+    public function handleFiles(array $files, array $cropping = null)
     {
         $this->errors = [];
         // picture upload
@@ -1970,7 +2040,11 @@ class Adherent
             if ($files['photo']['error'] === UPLOAD_ERR_OK) {
                 if ($files['photo']['tmp_name'] != '') {
                     if (is_uploaded_file($files['photo']['tmp_name'])) {
-                        $res = $this->picture->store($files['photo']);
+                        if ($this->preferences->pref_force_picture_ratio == 1 && isset($cropping)) {
+                            $res = $this->picture->store($files['photo'], false, $cropping);
+                        } else {
+                            $res = $this->picture->store($files['photo']);
+                        }
                         if ($res < 0) {
                             $this->errors[]
                                 = $this->picture->getErrorMessage($res);
@@ -2006,7 +2080,7 @@ class Adherent
      *
      * @return void
      */
-    public function setDuplicate()
+    public function setDuplicate(): void
     {
         //mark as duplicated
         $this->_duplicate = true;
@@ -2044,7 +2118,7 @@ class Adherent
      *
      * @return array
      */
-    public function getErrors()
+    public function getErrors(): array
     {
         return $this->errors;
     }
@@ -2054,8 +2128,11 @@ class Adherent
      *
      * @return array
      */
-    public function getGroups()
+    public function getGroups(): array
     {
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
         return $this->_groups;
     }
 
@@ -2064,8 +2141,11 @@ class Adherent
      *
      * @return array
      */
-    public function getManagedGroups()
+    public function getManagedGroups(): array
     {
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
         return $this->_managed_groups;
     }
 
@@ -2084,6 +2164,10 @@ class Adherent
             return true;
         }
 
+        if ($preferences->pref_bool_groupsmanagers_create_member && $login->isGroupManager()) {
+            return true;
+        }
+
         if ($preferences->pref_bool_create_member && $login->isLogged()) {
             return true;
         }
@@ -2100,6 +2184,8 @@ class Adherent
      */
     public function canEdit(Login $login): bool
     {
+        global $preferences;
+
         //admin and staff users can edit, as well as member itself
         if ($this->id && $login->id == $this->id || $login->isAdmin() || $login->isStaff()) {
             return true;
@@ -2110,8 +2196,8 @@ class Adherent
             return true;
         }
 
-        //group managers can edit members of groups they manage
-        if ($login->isGroupManager()) {
+        //group managers can edit members of groups they manage when pref is on
+        if ($preferences->pref_bool_groupsmanagers_edit_member && $login->isGroupManager()) {
             foreach ($this->getGroups() as $g) {
                 if ($login->isGroupManager($g->getId())) {
                     return true;
@@ -2131,6 +2217,15 @@ class Adherent
      */
     public function canShow(Login $login): bool
     {
+        //group managers can show members of groups they manage
+        if ($login->isGroupManager()) {
+            foreach ($this->getGroups() as $g) {
+                if ($login->isGroupManager($g->getId())) {
+                    return true;
+                }
+            }
+        }
+
         return $this->canEdit($login);
     }
 
@@ -2151,7 +2246,7 @@ class Adherent
      *
      * @return Adherent
      */
-    public function setSendmail($send = true): self
+    public function setSendmail(bool $send = true): self
     {
         $this->sendmail = $send;
         return $this;
@@ -2162,7 +2257,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function sendEMail()
+    public function sendEMail(): bool
     {
         return $this->sendmail;
     }
@@ -2182,70 +2277,12 @@ class Adherent
     }
 
     /**
-     * Reset dependencies to load
-     *
-     * @return $this
-     */
-    public function disableAllDeps(): self
-    {
-        foreach ($this->_deps as &$dep) {
-            $dep = false;
-        }
-        return $this;
-    }
-
-    /**
-     * Enable all dependencies to load
-     *
-     * @return $this
-     */
-    public function enableAllDeps(): self
-    {
-        foreach ($this->_deps as &$dep) {
-            $dep = true;
-        }
-        return $this;
-    }
-
-    /**
-     * Enable a load dependency
+     * Get prefix for events
      *
-     * @param string $name Dependency name
-     *
-     * @return $this
-     */
-    public function enableDep(string $name): self
-    {
-        if (!isset($this->_deps[$name])) {
-            Analog::log(
-                'dependency ' . $name . ' does not exists!',
-                Analog::WARNING
-            );
-        } else {
-            $this->_deps[$name] = true;
-        }
-
-        return $this;
-    }
-
-    /**
-     * Enable a load dependency
-     *
-     * @param string $name Dependency name
-     *
-     * @return $this
+     * @return string
      */
-    public function disableDep(string $name): self
+    protected function getEventsPrefix(): string
     {
-        if (!isset($this->_deps[$name])) {
-            Analog::log(
-                'dependency ' . $name . ' does not exists!',
-                Analog::WARNING
-            );
-        } else {
-            $this->_deps[$name] = false;
-        }
-
-        return $this;
+        return 'member';
     }
 }