]> 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 9fc2d2bad5d1950ed53178f1d5f0e042a6747ec7..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
 
 namespace Galette\Entity;
 
+use ArrayObject;
+use Galette\Events\GaletteEvent;
+use Galette\Features\HasEvent;
+use Galette\Features\Socials;
 use Throwable;
 use Analog\Analog;
 use Laminas\Db\Sql\Expression;
@@ -48,6 +52,7 @@ use Galette\Core\History;
 use Galette\Repository\Groups;
 use Galette\Core\Login;
 use Galette\Repository\Members;
+use Galette\Features\Dynamics;
 
 /**
  * Member class for galette
@@ -56,7 +61,7 @@ use Galette\Repository\Members;
  * @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
@@ -64,12 +69,12 @@ use Galette\Repository\Members;
  * @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
@@ -78,17 +83,12 @@ use Galette\Repository\Members;
  * @property integer $status
  * @property string $sstatus Status label
  * @property string $address
- * @property string $address_continuation
  * @property string $zipcode
  * @property string $town
  * @property string $country
  * @property string $phone
  * @property string $gsm
  * @property string $email
- * @property string $website
- * @property string $msn
- * @property string $icq
- * @property string $jabber
  * @property string $gnupgid
  * @property string $fingerprint
  * @property string $login
@@ -115,14 +115,19 @@ use Galette\Repository\Members;
  * @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 DynamicsTrait;
+    use Dynamics;
+    use Socials;
+    use HasEvent;
 
     public const TABLE = 'adherents';
     public const PK = 'id_adh';
@@ -154,23 +159,18 @@ class Adherent
     private $_status;
     //Contact information
     private $_address;
-    private $_address_continuation; /** TODO: remove */
     private $_zipcode;
     private $_town;
     private $_country;
     private $_phone;
     private $_gsm;
     private $_email;
-    private $_website;
-    private $_msn; /** TODO: remove */
-    private $_icq; /** TODO: remove */
-    private $_jabber; /** TODO: remove */
-    private $_gnupgid; /** TODO: remove */
-    private $_fingerprint; /** TODO: remove */
+    private $_gnupgid;
+    private $_fingerprint;
     //Galette relative information
     private $_appears_in_list;
     private $_admin;
-    private $_staff;
+    private $_staff = false;
     private $_due_free;
     private $_login;
     private $_password;
@@ -182,23 +182,17 @@ 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;
 
     private $_row_classes;
 
     private $_self_adh = false;
-    private $_deps = array(
-        'picture'   => true,
-        'groups'    => true,
-        'dues'      => true,
-        'parent'    => false,
-        'children'  => false,
-        'dynamics'  => false
-    );
 
     private $zdb;
     private $preferences;
@@ -207,7 +201,6 @@ class Adherent
 
     private $parent_fields = [
         'adresse_adh',
-        'adresse2_adh',
         'cp_adh',
         'ville_adh',
         'email_adh'
@@ -220,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 instanciate 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)
     {
@@ -234,24 +227,24 @@ 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 shoud be an array, ' . gettype($deps) . ' given!',
+                    '$deps should be an array, ' . gettype($deps) . ' given!',
                     Analog::WARNING
                 );
             }
         }
 
+        $this
+            ->withAddEvent()
+            ->withEditEvent()
+            ->withoutDeleteEvent()
+            ->activateEvents();
+
         if ($args == null || is_int($args)) {
             if (is_int($args) && $args > 0) {
                 $this->load($args);
@@ -289,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');
@@ -306,14 +299,16 @@ class Adherent
                 return false;
             }
 
-            $this->loadFromRS($results->current());
+            /** @var ArrayObject $result */
+            $result = $results->current();
+            $this->loadFromRS($result);
             return true;
         } catch (Throwable $e) {
             Analog::log(
                 'Cannot load member form id `' . $id . '` | ' . $e->getMessage(),
                 Analog::WARNING
             );
-            return false;
+            throw $e;
         }
     }
 
@@ -322,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);
@@ -337,28 +332,30 @@ 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 . '` | ' .
                 $e->getMessage(),
                 Analog::WARNING
             );
-            return false;
+            throw $e;
         }
     }
 
     /**
      * 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;
@@ -377,39 +374,28 @@ 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;
         $this->_phone = $r->tel_adh;
         $this->_gsm = $r->gsm_adh;
         $this->_email = $r->email_adh;
-        $this->_website = $r->url_adh;
-        /** TODO: remove */
-        $this->_msn = $r->msn_adh;
-        /** TODO: remove */
-        $this->_icq = $r->icq_adh;
-        /** TODO: remove */
-        $this->_jabber = $r->jabber_adh;
-        /** TODO: remove */
         $this->_gnupgid = $r->gpgid;
-        /** TODO: remove */
         $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;
@@ -421,6 +407,7 @@ class Adherent
         $this->_due_date = $r->date_echeance;
         $this->_others_infos = $r->info_public_adh;
         $this->_others_infos_admin = $r->info_adh;
+        $this->_number = $r->num_adh;
 
         if ($r->parent_id !== null) {
             $this->_parent = (int)$r->parent_id;
@@ -448,6 +435,10 @@ class Adherent
         if ($this->_deps['dynamics'] === true) {
             $this->loadDynamicFields();
         }
+
+        if ($this->_deps['socials'] === true) {
+            $this->loadSocials();
+        }
     }
 
     /**
@@ -455,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);
         }
@@ -468,7 +459,7 @@ class Adherent
      *
      * @return void
      */
-    private function loadChildren()
+    private function loadChildren(): void
     {
         $this->_children = array();
         try {
@@ -476,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);
 
@@ -496,7 +485,7 @@ class Adherent
                 $e->getMessage(),
                 Analog::WARNING
             );
-            return false;
+            throw $e;
         }
     }
 
@@ -505,19 +494,29 @@ class Adherent
      *
      * @return void
      */
-    public function loadGroups()
+    public function loadGroups(): void
     {
         $this->_groups = Groups::loadGroups($this->_id);
         $this->_managed_groups = Groups::loadManagedGroups($this->_id);
     }
 
+    /**
+     * Load member social network/contact information
+     *
+     * @return void
+     */
+    public function loadSocials(): void
+    {
+        $this->_socials = Social::getListForMember($this->_id);
+    }
+
     /**
      * Retrieve status from preferences
      *
-     * @return pref_statut
+     * @return integer
      *
      */
-    private function getDefaultStatus()
+    private function getDefaultStatus(): int
     {
         global $preferences;
         if ($preferences->pref_statut != '') {
@@ -536,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;
 
@@ -552,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';
                 }
             }
         }
@@ -577,7 +583,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isAdmin()
+    public function isAdmin(): bool
     {
         return $this->_admin;
     }
@@ -587,7 +593,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isStaff()
+    public function isStaff(): bool
     {
         return $this->_staff;
     }
@@ -597,7 +603,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isDueFree()
+    public function isDueFree(): bool
     {
         return $this->_due_free;
     }
@@ -609,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;
     }
 
     /**
@@ -634,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;
     }
 
     /**
@@ -657,7 +655,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isCompany()
+    public function isCompany(): bool
     {
         return trim($this->_company_name ?? '') != '';
     }
@@ -667,7 +665,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isMan()
+    public function isMan(): bool
     {
         return (int)$this->_gender === self::MAN;
     }
@@ -677,7 +675,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function isWoman()
+    public function isWoman(): bool
     {
         return (int)$this->_gender === self::WOMAN;
     }
@@ -688,7 +686,7 @@ class Adherent
      *
      * @return bool
      */
-    public function appearsInMembersList()
+    public function appearsInMembersList(): bool
     {
         return $this->_appears_in_list;
     }
@@ -698,7 +696,7 @@ class Adherent
      *
      * @return bool
      */
-    public function isActive()
+    public function isActive(): bool
     {
         return $this->_active;
     }
@@ -708,7 +706,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasPicture()
+    public function hasPicture(): bool
     {
         return $this->_picture->hasPicture();
     }
@@ -718,7 +716,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasParent()
+    public function hasParent(): bool
     {
         return !empty($this->_parent);
     }
@@ -728,7 +726,7 @@ class Adherent
      *
      * @return bool
      */
-    public function hasChildren()
+    public function hasChildren(): bool
     {
         if ($this->_children === null) {
             if ($this->id) {
@@ -750,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;
         }
@@ -764,15 +762,23 @@ class Adherent
      *
      * @return string i18n string representing state of due
      */
-    public function getDues()
+    public function getDues(): string
     {
         $ret = '';
-        $date_now = new \DateTime();
-        $ddate = new \DateTime($this->_due_date);
-        $date_diff = $date_now->diff($ddate);
+        $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(
@@ -788,17 +794,32 @@ class Adherent
             } else {
                 $ret = _T("Never contributed");
             }
+        // Last active or first expired day
         } elseif ($this->_days_remaining == 0) {
             if ($date_diff->invert == 0) {
                 $ret = _T("Last day!");
             } else {
                 $ret = _T("Late since today!");
             }
-        } elseif ($this->_days_remaining < 0) {
+        // Active
+        } elseif ($date_diff->invert == 0 && $this->_days_remaining > 0) {
+            $patterns = array('/%days/', '/%date/');
+            $replace = array(
+                $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(
-                $this->_days_remaining * -1,
-                $ddate->format(__("Y-m-d"))
+                // 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(
@@ -809,17 +830,6 @@ class Adherent
             } else {
                 $ret = _T("No longer member");
             }
-        } else {
-            $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;
     }
@@ -834,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();
@@ -855,7 +865,7 @@ class Adherent
                 $e->getMessage(),
                 Analog::WARNING
             );
-            return false;
+            throw $e;
         }
     }
 
@@ -863,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);
@@ -902,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);
@@ -916,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.',
@@ -929,7 +944,7 @@ class Adherent
                 '` | ' . $e->getMessage(),
                 Analog::ERROR
             );
-            return false;
+            throw $e;
         }
     }
 
@@ -940,11 +955,13 @@ class Adherent
      *
      * @return string
      */
-    private function getFieldLabel($field)
+    private function getFieldLabel(string $field): string
     {
-        $label = $this->fields[$field]['label'];
-        //remove trailing ':' and then nbsp (for french at least)
-        $label = trim(trim($label ?? '', ':'), '&nbsp;');
+        $label = $this->fields[$field]['label'] ?? '';
+        //replace "&nbsp;"
+        $label = str_replace('&nbsp;', ' ', $label);
+        //remove trailing ':' and then trim
+        $label = trim(trim($label, ':'));
         return $label;
     }
 
@@ -955,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();
@@ -970,7 +987,7 @@ class Adherent
      *
      * @return void
      */
-    public function setSelfMembership()
+    public function setSelfMembership(): void
     {
         $this->_self_adh = true;
     }
@@ -980,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!'
-            );
         }
     }
 
@@ -1033,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
@@ -1074,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)) {
@@ -1096,6 +1114,9 @@ class Adherent
                         //values that are set at object instantiation
                         $value = $this->$prop;
                         break;
+                    case self::PK:
+                        $value = null;
+                        break;
                     default:
                         $value = '';
                         break;
@@ -1119,7 +1140,9 @@ class Adherent
 
                 // now, check validity
                 if ($value !== null && $value != '') {
-                    $this->validate($key, $value, $values);
+                    if ($key !== 'mdp_adh') {
+                        $this->validate($key, $value, $values);
+                    }
                 } elseif (empty($this->_id)) {
                     //ensure login and password are not empty
                     if (($key == 'login_adh' || $key == 'mdp_adh') && !isset($required[$key])) {
@@ -1136,6 +1159,11 @@ class Adherent
             }
         }
 
+        //password checks need data to be previously set
+        if (isset($values['mdp_adh'])) {
+            $this->validate('mdp_adh', $values['mdp_adh'], $values);
+        }
+
         // missing required fields?
         foreach ($required as $key => $val) {
             $prop = '_' . $this->fields[$key]['propname'];
@@ -1163,7 +1191,25 @@ 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);
 
         if (count($this->errors) > 0) {
             Analog::log(
@@ -1193,7 +1239,7 @@ class Adherent
      *
      * @return void
      */
-    public function validate($field, $value, $values)
+    public function validate(string $field, $value, array $values): void
     {
         global $preferences;
 
@@ -1274,45 +1320,37 @@ class Adherent
                 }
                 break;
             case 'email_adh':
-            case 'msn_adh':
                 if (!GaletteMail::isValidEmail($value)) {
                     $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.");
                     }
-                }
-                break;
-            case 'url_adh':
-                if ($value == 'http://') {
-                    $this->$prop = '';
-                } elseif (!isValidWebUrl($value)) {
-                    $this->errors[] = _T("- Non-valid Website address! Maybe you've skipped the http://?");
+
+                    $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':
-                /** FIXME: add a preference for login lenght */
+                /** FIXME: add a preference for login length */
                 if (strlen($value) < 2) {
                     $this->errors[] = str_replace(
                         '%i',
@@ -1331,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
                                 );
                             }
 
@@ -1345,7 +1384,7 @@ class Adherent
                             }
                         } catch (Throwable $e) {
                             Analog::log(
-                                'An error occurred checking member login unicity.',
+                                'An error occurred checking member login uniqueness.',
                                 Analog::ERROR
                             );
                             $this->errors[] = _T("An error has occurred while looking if login already exists.");
@@ -1390,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();
@@ -1404,7 +1443,7 @@ class Adherent
                     }
                 } catch (Throwable $e) {
                     Analog::log(
-                        'An error occurred checking status existance: ' . $e->getMessage(),
+                        'An error occurred checking status existence: ' . $e->getMessage(),
                         Analog::ERROR
                     );
                     $this->errors[] = _T("An error has occurred while looking if status does exists.");
@@ -1429,7 +1468,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function store()
+    public function store(): bool
     {
         global $hist, $emitter, $login;
         $event = null;
@@ -1493,6 +1532,10 @@ class Adherent
                 $values['parent_id'] = new Expression('NULL');
             }
 
+            if (!$this->_number) {
+                $values['num_adh'] = new Expression('NULL');
+            }
+
             //fields that cannot be null
             $notnull = [
                 '_surname'  => 'prenom_adh',
@@ -1507,7 +1550,6 @@ class Adherent
                 }
             }
 
-            $success = false;
             if (empty($this->_id)) {
                 //we're inserting a new member
                 unset($values[self::PK]);
@@ -1534,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(
@@ -1559,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);
 
@@ -1574,27 +1613,25 @@ class Adherent
                         $this->sname
                     );
                 }
-                $success = true;
-                $event = 'member.edit';
+                $event = $this->getEditEventName();
             }
 
             //dynamic fields
-            if ($success) {
-                $success = $this->dynamicsStore();
-            }
+            $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" .
                 $e->getTraceAsString(),
                 Analog::ERROR
             );
-            return false;
+            throw $e;
         }
     }
 
@@ -1603,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(
@@ -1620,21 +1657,22 @@ class Adherent
                 $e->getMessage() . "\n" . $e->getTraceAsString(),
                 Analog::ERROR
             );
+            throw $e;
         }
     }
 
     /**
      * Global getter method
      *
-     * @param string $name name of the property we want to retrive
+     * @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
@@ -1643,10 +1681,16 @@ class Adherent
         $virtuals = array(
             'sadmin', 'sstaff', 'sdue_free', 'sappears_in_list', 'sactive',
             'stitle', 'sstatus', 'sfullname', 'sname', 'saddress',
-            'rbirthdate', 'sgender', 'contribstatus'
+            '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':
                     return $this->isAdmin();
@@ -1660,126 +1704,197 @@ 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!");
             }
+        }
+
+        if (in_array($name, $virtuals)) {
+            if (substr($name, 0, 1) !== '_') {
+                $real = '_' . substr($name, 1);
+            } else {
+                $real = $name;
+            }
+            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"));
+                case 'sactive':
+                    return (($this->isActive()) ? _T("Active") : _T("Inactive"));
+                case 'stitle':
+                    if (isset($this->_title) && $this->_title instanceof Title) {
+                        return $this->_title->tshort;
+                    } else {
+                        return null;
+                    }
+                case 'sstatus':
+                    $status = new Status($this->zdb);
+                    return $status->getLabel($this->_status);
+                case 'sfullname':
+                    return $this->getNameWithCase(
+                        $this->_name,
+                        $this->_surname,
+                        (isset($this->_title) ? $this->title : false)
+                    );
+                case 'saddress':
+                    $address = $this->_address;
+                    return $address;
+                case 'sname':
+                    return $this->getNameWithCase($this->_name, $this->_surname);
+                case 'rbirthdate':
+                    return $this->_birthdate;
+                case 'sgender':
+                    switch ($this->gender) {
+                        case self::MAN:
+                            return _T('Man');
+                        case self::WOMAN:
+                            return _T('Woman');
+                        default:
+                            return _T('Unspecified');
+                    }
+                case 'contribstatus':
+                    return $this->getDues();
+            }
+        }
+
+        //for backward compatibility
+        if (in_array($name, $socials)) {
+            $values = Social::getListForMember($this->_id, $name);
+            return $values[0] ?? null;
+        }
+
+        if (substr($name, 0, 1) !== '_') {
+            $rname = '_' . $name;
         } else {
-            if (in_array($name, $virtuals)) {
-                if (substr($name, 0, 1) !== '_') {
-                    $real = '_' . substr($name, 1);
+            $rname = $name;
+        }
+
+        switch ($name) {
+            case 'id':
+            case 'id_statut':
+                if ($this->$rname !== null) {
+                    return (int)$this->$rname;
                 } else {
-                    $real = $name;
+                    return null;
                 }
-                switch ($name) {
-                    case 'sadmin':
-                    case 'sdue_free':
-                    case 'sappears_in_list':
-                    case 'sstaff':
-                        return (($this->$real) ? _T("Yes") : _T("No"));
-                        break;
-                    case 'sactive':
-                        return (($this->$real) ? _T("Active") : _T("Inactive"));
-                        break;
-                    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)
+            case 'address':
+                return $this->$rname ?? '';
+            case 'birthdate':
+            case 'creation_date':
+            case 'modification_date':
+            case 'due_date':
+                if ($this->$rname != '') {
+                    try {
+                        $d = new \DateTime($this->$rname);
+                        return $d->format(__("Y-m-d"));
+                    } catch (Throwable $e) {
+                        //oops, we've got a bad date :/
+                        Analog::log(
+                            'Bad date (' . $this->$rname . ') | ' .
+                            $e->getMessage(),
+                            Analog::INFO
                         );
-                        break;
-                    case 'saddress':
-                        $address = $this->_address;
-                        if ($this->_address_continuation !== '' && $this->_address_continuation !== null) {
-                            $address .= "\n" . $this->_address_continuation;
-                        }
-                        return htmlspecialchars($address, ENT_QUOTES);
-                        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:
-                                return _T('Man');
-                            case self::WOMAN:
-                                return _T('Woman');
-                            default:
-                                return _T('Unspecified');
-                        }
-                        break;
-                    case 'contribstatus':
-                        return $this->getDues();
-                        break;
+                        return $this->$rname;
+                    }
                 }
-            } else {
-                if (substr($name, 0, 1) !== '_') {
-                    $rname = '_' . $name;
+                break;
+            case 'parent_id':
+                return ($this->_parent instanceof Adherent) ? (int)$this->_parent->id : (int)$this->_parent;
+            default:
+                if (!property_exists($this, $rname)) {
+                    Analog::log(
+                        "Unknown property '$rname'",
+                        Analog::WARNING
+                    );
+                    return null;
                 } else {
-                    $rname = $name;
+                    return $this->$rname;
                 }
+        }
+    }
 
-                switch ($name) {
-                    case 'id':
-                    case 'id_statut':
-                        if ($this->$rname !== null) {
-                            return (int)$this->$rname;
-                        } else {
-                            return null;
-                        }
-                        break;
-                    case 'address':
-                    case 'address_continuation':
-                        return $this->$rname ?? '';
-                        break;
-                    case 'birthdate':
-                    case 'creation_date':
-                    case 'modification_date':
-                    case 'due_date':
-                        if ($this->$rname != '') {
-                            try {
-                                $d = new \DateTime($this->$rname);
-                                return $d->format(__("Y-m-d"));
-                            } catch (Throwable $e) {
-                                //oops, we've got a bad date :/
-                                Analog::log(
-                                    'Bad date (' . $this->$rname . ') | ' .
-                                    $e->getMessage(),
-                                    Analog::INFO
-                                );
-                                return $this->$rname;
-                            }
-                        }
-                        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(
-                                "Unknown property '$rname'",
-                                Analog::WARNING
-                            );
-                            return null;
-                        } else {
-                            return $this->$rname;
-                        }
-                        break;
-                }
+    /**
+     * Global isset method
+     * Required for twig to access properties via __get
+     *
+     * @param string $name name of the property we want to retrieve
+     *
+     * @return bool
+     */
+    public function __isset(string $name)
+    {
+        $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;
+        }
+
+        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);
         }
     }
 
@@ -1790,15 +1905,15 @@ class Adherent
      *
      * @return string
      */
-    public function getEmail()
+    public function getEmail(): string
     {
         $email = $this->_email;
-        if (empty($email)) {
+        if (empty($email) && $this->hasParent()) {
             $this->loadParent();
             $email = $this->parent->email;
         }
 
-        return $email;
+        return $email ?? '';
     }
 
     /**
@@ -1807,7 +1922,7 @@ class Adherent
      *
      * @return string
      */
-    public function getAddress()
+    public function getAddress(): string
     {
         $address = $this->_address;
         if (empty($address) && $this->hasParent()) {
@@ -1815,25 +1930,7 @@ class Adherent
             $address = $this->parent->address;
         }
 
-        return $address;
-    }
-
-    /**
-     * Get member address continuation.
-     * 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()
-    {
-        $address = $this->_address;
-        $address_continuation = $this->_address_continuation;
-        if (empty($address) && $this->hasParent()) {
-            $this->loadParent();
-            $address_continuation = $this->parent->address_continuation;
-        }
-
-        return $address_continuation;
+        return $address ?? '';
     }
 
     /**
@@ -1842,7 +1939,7 @@ class Adherent
      *
      * @return string
      */
-    public function getZipcode()
+    public function getZipcode(): string
     {
         $address = $this->_address;
         $zip = $this->_zipcode;
@@ -1851,7 +1948,7 @@ class Adherent
             $zip = $this->parent->zipcode;
         }
 
-        return $zip;
+        return $zip ?? '';
     }
 
     /**
@@ -1860,7 +1957,7 @@ class Adherent
      *
      * @return string
      */
-    public function getTown()
+    public function getTown(): string
     {
         $address = $this->_address;
         $town = $this->_town;
@@ -1869,7 +1966,7 @@ class Adherent
             $town = $this->parent->town;
         }
 
-        return $town;
+        return $town ?? '';
     }
 
     /**
@@ -1878,7 +1975,7 @@ class Adherent
      *
      * @return string
      */
-    public function getCountry()
+    public function getCountry(): string
     {
         $address = $this->_address;
         $country = $this->_country;
@@ -1887,7 +1984,7 @@ class Adherent
             $country = $this->parent->country;
         }
 
-        return $country;
+        return $country ?? '';
     }
 
     /**
@@ -1895,7 +1992,7 @@ class Adherent
      *
      * @return string
      */
-    public function getAge()
+    public function getAge(): string
     {
         if ($this->_birthdate == null) {
             return '';
@@ -1907,7 +2004,7 @@ class Adherent
                 'Invalid birthdate: ' . $this->_birthdate,
                 Analog::ERROR
             );
-            return;
+            return '';
         }
 
         return str_replace(
@@ -1922,7 +2019,7 @@ class Adherent
      *
      * @return array
      */
-    public function getParentFields()
+    public function getParentFields(): array
     {
         return $this->parent_fields;
     }
@@ -1930,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
@@ -1942,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);
@@ -1978,7 +2080,7 @@ class Adherent
      *
      * @return void
      */
-    public function setDuplicate()
+    public function setDuplicate(): void
     {
         //mark as duplicated
         $this->_duplicate = true;
@@ -2016,7 +2118,7 @@ class Adherent
      *
      * @return array
      */
-    public function getErrors()
+    public function getErrors(): array
     {
         return $this->errors;
     }
@@ -2026,8 +2128,11 @@ class Adherent
      *
      * @return array
      */
-    public function getGroups()
+    public function getGroups(): array
     {
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
         return $this->_groups;
     }
 
@@ -2036,8 +2141,11 @@ class Adherent
      *
      * @return array
      */
-    public function getManagedGroups()
+    public function getManagedGroups(): array
     {
+        if (!$this->isDepEnabled('groups')) {
+            $this->loadGroups();
+        }
         return $this->_managed_groups;
     }
 
@@ -2056,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;
         }
@@ -2072,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;
@@ -2082,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;
@@ -2103,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);
     }
 
@@ -2123,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;
@@ -2134,7 +2257,7 @@ class Adherent
      *
      * @return boolean
      */
-    public function sendEMail()
+    public function sendEMail(): bool
     {
         return $this->sendmail;
     }
@@ -2154,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
-     *
-     * @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
+     * Get prefix for events
      *
-     * @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';
     }
 }