userRepository = $userRepository; $this->entityManager = $entityManager; $this->logger = $logger; $this->cookieLifetime = $sessionStorageOptions['cookie_lifetime'] ?? 0; $this->sessionHandler = $sessionHandler; } /** * Удаляет неактивных пользователей с только ROLE_USER после истечения времени жизни кукисов * * @return int Количество удаленных пользователей */ public function cleanupExpiredUsers(): int { // Если cookie_lifetime = 0, используем время жизни сессии по умолчанию (1 час) // Иначе используем указанное время жизни кукисов $sessionLifetime = $this->cookieLifetime > 0 ? $this->cookieLifetime : 3600; // 1 час по умолчанию для session cookies // Вычисляем дату, до которой должна быть последняя активность пользователя $beforeDate = new \DateTime(); $beforeDate->modify("-{$sessionLifetime} seconds"); $usersToDelete = $this->userRepository->findUsersToCleanup($beforeDate); $deletedCount = 0; $this->logger->info(sprintf( 'Найдено пользователей для проверки: %d (время жизни сессии: %d секунд, дата отсечки: %s)', count($usersToDelete), $sessionLifetime, $beforeDate->format('Y-m-d H:i:s') )); foreach ($usersToDelete as $user) { // Проверяем, что у пользователя только ROLE_USER // Если массив ролей пуст, значит у пользователя только ROLE_USER (который добавляется автоматически) $actualRoles = $user->getActualRoles(); $hasOnlyUserRole = empty($actualRoles); $this->logger->debug(sprintf( 'Проверка пользователя ID %d: роли=%s, только ROLE_USER=%s', $user->getId(), json_encode($actualRoles), $hasOnlyUserRole ? 'да' : 'нет' )); if ($hasOnlyUserRole) { // Определяем дату для логирования $activityDate = $user->getLastActivityAt() ?? $user->getCreatedAt(); $activityDateStr = $activityDate ? $activityDate->format('Y-m-d H:i:s') : 'N/A'; try { // Удаляем сессии пользователя из Redis перед удалением пользователя $this->cleanupUserSessions($user); $this->deleteUserAndRelatedData($user); $deletedCount++; $this->logger->info(sprintf( 'Удален неактивный пользователь с ID %d (uid: %d, email: %s, последняя активность: %s)', $user->getId(), $user->getUid(), $user->getEmail() ?? 'N/A', $activityDateStr )); } catch (\Exception $e) { $this->logger->error(sprintf( 'Ошибка при удалении пользователя с ID %d: %s', $user->getId(), $e->getMessage() )); } } } // Очищаем старые сессии из Redis $this->cleanupOldRedisSessions($sessionLifetime); return $deletedCount; } /** * Удаляет пользователя и все связанные данные * * @param User $user */ private function deleteUserAndRelatedData(User $user): void { // Удаляем связанные данные Calltouch (если есть) $this->deleteCalltouchData($user); // Удаляем сам пользователя $this->entityManager->remove($user); $this->entityManager->flush(); } /** * Очищает сессии пользователя из Redis * * @param User $user */ private function cleanupUserSessions(User $user): void { if (!$this->sessionHandler) { return; } try { $redis = $this->getRedisClient(); if (!$redis) { return; } // Ищем все сессии, которые могут принадлежать пользователю // В Symfony сессии обычно хранятся с префиксом PHPSESSID или sess_ $patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*']; $deletedSessions = 0; foreach ($patterns as $pattern) { $keys = $redis->keys($pattern); if (!$keys) { continue; } foreach ($keys as $key) { try { $sessionData = $redis->get($key); if ($sessionData) { // Пытаемся найти user_id в данных сессии // Сессии Symfony обычно содержат сериализованные данные if (strpos($sessionData, '_security_main') !== false || strpos($sessionData, (string)$user->getId()) !== false || strpos($sessionData, (string)$user->getUid()) !== false) { $redis->del($key); $deletedSessions++; } } } catch (\Exception $e) { // Игнорируем ошибки при проверке отдельных ключей } } } if ($deletedSessions > 0) { $this->logger->info(sprintf( 'Удалено %d сессий из Redis для пользователя %d', $deletedSessions, $user->getId() )); } } catch (\Exception $e) { $this->logger->warning(sprintf( 'Ошибка при очистке сессий пользователя %d из Redis: %s', $user->getId(), $e->getMessage() )); } } /** * Очищает старые сессии из Redis * * @param int $sessionLifetime Время жизни сессии в секундах */ private function cleanupOldRedisSessions(int $sessionLifetime): void { if (!$this->sessionHandler) { return; } try { $redis = $this->getRedisClient(); if (!$redis) { return; } $patterns = ['PHPSESSID_*', 'sess_*', '*_symfony_*']; $deletedCount = 0; $now = time(); foreach ($patterns as $pattern) { $keys = $redis->keys($pattern); if (!$keys) { continue; } foreach ($keys as $key) { try { $ttl = $redis->ttl($key); // Если TTL отрицательный (ключ не имеет TTL) или больше времени жизни сессии, пропускаем if ($ttl < 0 || $ttl > $sessionLifetime) { continue; } // Проверяем, не истекла ли сессия $lastAccess = $now - $ttl; if ($lastAccess > $sessionLifetime) { $redis->del($key); $deletedCount++; } } catch (\Exception $e) { // Игнорируем ошибки при проверке отдельных ключей } } } if ($deletedCount > 0) { $this->logger->info(sprintf( 'Удалено %d старых сессий из Redis', $deletedCount )); } } catch (\Exception $e) { $this->logger->warning(sprintf( 'Ошибка при очистке старых сессий из Redis: %s', $e->getMessage() )); } } /** * Получает Redis клиент из session handler * * @return \Redis|\Predis\Client|null */ private function getRedisClient() { if (!$this->sessionHandler) { return null; } try { // Пытаемся получить Redis клиент из различных типов session handlers if ($this->sessionHandler instanceof \Redis) { return $this->sessionHandler; } if ($this->sessionHandler instanceof \Predis\Client) { return $this->sessionHandler; } // Для RedisSessionHandler из Symfony if ($this->sessionHandler instanceof RedisSessionHandler) { $reflection = new \ReflectionClass($this->sessionHandler); if ($reflection->hasProperty('redis')) { $property = $reflection->getProperty('redis'); $property->setAccessible(true); return $property->getValue($this->sessionHandler); } } // Пытаемся вызвать метод getRedis, если он существует if (method_exists($this->sessionHandler, 'getRedis')) { return $this->sessionHandler->getRedis(); } return null; } catch (\Exception $e) { $this->logger->warning(sprintf( 'Не удалось получить Redis клиент: %s', $e->getMessage() )); return null; } } /** * Удаляет данные Calltouch, связанные с пользователем * * @param User $user */ private function deleteCalltouchData(User $user): void { try { // Проверяем наличие связи с Calltouch через миграции // Если есть таблица calltouch с внешним ключом на users.id $connection = $this->entityManager->getConnection(); // Удаляем записи calltouch, связанные с пользователем $connection->executeStatement( 'DELETE FROM calltouch WHERE user_id = :userId', ['userId' => $user->getId()] ); } catch (\Exception $e) { // Игнорируем ошибки, если таблица не существует или нет связи $this->logger->warning(sprintf( 'Не удалось удалить данные Calltouch для пользователя %d: %s', $user->getId(), $e->getMessage() )); } } }