chore: initial import for test contour with k3s CI
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\PriceList;
|
||||
use App\Repository\PriceListRepository;
|
||||
use App\Repository\FilialRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use App\Bundle\Infoclinica\Region;
|
||||
|
||||
class PriceListService
|
||||
{
|
||||
private $priceListRepository;
|
||||
private $filialRepository;
|
||||
|
||||
public function __construct(PriceListRepository $priceListRepository, FilialRepository $filialRepository)
|
||||
{
|
||||
$this->priceListRepository = $priceListRepository;
|
||||
$this->filialRepository = $filialRepository;
|
||||
}
|
||||
|
||||
public function getFilteredPriceListQuery(array $filters): QueryBuilder
|
||||
{
|
||||
$filters['actual'] = true;
|
||||
|
||||
if (empty($filters['filial'])) {
|
||||
$filters['filial'] = $this->getCurrentFilialIds();
|
||||
}
|
||||
|
||||
$priceListQuery = $this->priceListRepository->createFilteredQueryBuilder($filters);
|
||||
|
||||
return $priceListQuery;
|
||||
}
|
||||
|
||||
public function getPriceListQuery(array $filters): QueryBuilder
|
||||
{
|
||||
$priceListQuery = $this->priceListRepository->createFilteredQueryBuilder($filters);
|
||||
|
||||
return $priceListQuery;
|
||||
}
|
||||
|
||||
private function getCurrentFilialIds(): array
|
||||
{
|
||||
$filials = $this->filialRepository->findByRegion(Region::getCurrentName());
|
||||
|
||||
return array_map(function ($filial) {
|
||||
return $filial->getFid();
|
||||
}, $filials);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\LocationView;
|
||||
use App\Entity\PriceList;
|
||||
use App\Entity\Review;
|
||||
use App\Repository\LocationViewRepository;
|
||||
use App\Repository\PriceListRepository;
|
||||
use App\Repository\ReviewRepository;
|
||||
use App\Bundle\Utils\Logger;
|
||||
|
||||
class SpecialistMoreService
|
||||
{
|
||||
public array $prices = [];
|
||||
private array $locations = [];
|
||||
public int $specialist;
|
||||
public ?array $kodopers = null;
|
||||
public ?array $reviews = null;
|
||||
public bool $onlineMode;
|
||||
|
||||
public function __construct(
|
||||
private LocationViewRepository $locationViewRepository,
|
||||
private ReviewRepository $reviewRepository,
|
||||
private PriceListRepository $priceListRepository,
|
||||
bool $onlineMode = false
|
||||
) {
|
||||
$this->onlineMode = $onlineMode;
|
||||
}
|
||||
|
||||
public function setSpecialist(int $specialistId, ?array $kodopers): static
|
||||
{
|
||||
$this->specialistId = $specialistId;
|
||||
$this->kodopers = $kodopers;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLocations(): array
|
||||
{
|
||||
if (empty($this->locations)) {
|
||||
$this->locations = $this
|
||||
->locationViewRepository
|
||||
->findBySpecialistId($this->specialistId, $this->onlineMode);
|
||||
}
|
||||
|
||||
return $this->locations;
|
||||
}
|
||||
|
||||
public function locationsCount(): int
|
||||
{
|
||||
return count($this->getLocations());
|
||||
}
|
||||
|
||||
public function defaultLocation(): ?array
|
||||
{
|
||||
$this->getLocations();
|
||||
|
||||
usort($this->locations, function($a, $b) {
|
||||
return $a['nearestDate'] <=> $b['nearestDate'];
|
||||
});
|
||||
|
||||
return !empty($this->locations) ? $this->locations[0] : null;
|
||||
}
|
||||
|
||||
public function hasPrice(): bool
|
||||
{
|
||||
return !empty($this->getPrices());
|
||||
}
|
||||
|
||||
public function getPrices(): ?array
|
||||
{
|
||||
if (empty($this->prices)) {
|
||||
if ($this->kodopers) {
|
||||
// Логируем входные данные
|
||||
Logger::send([
|
||||
'method' => 'SpecialistMoreService::getPrices()',
|
||||
'action' => 'Input kodopers',
|
||||
'kodopers' => $this->kodopers
|
||||
], 'dev.log');
|
||||
|
||||
// Фильтр по актуальным записям (обновленным за последние 2 дня)
|
||||
$filters['actual'] = true;
|
||||
$filters['kodoper'] = $this->kodopers;
|
||||
$filters['filial'] = [];
|
||||
|
||||
// Убеждаемся, что locations загружены перед использованием
|
||||
$locations = $this->getLocations();
|
||||
|
||||
foreach ($locations as $location) {
|
||||
if (isset($location['filial']) && $location['filial'] !== null) {
|
||||
$filters['filial'][] = $location['filial'];
|
||||
}
|
||||
}
|
||||
|
||||
// Убираем дубликаты филиалов
|
||||
$filters['filial'] = array_values(array_unique($filters['filial']));
|
||||
|
||||
// Если филиалы не найдены, не применяем фильтр по филиалам
|
||||
// чтобы показать все цены для данного специалиста
|
||||
if (empty($filters['filial'])) {
|
||||
unset($filters['filial']);
|
||||
}
|
||||
|
||||
$queryBuilder = $this->priceListRepository->createFilteredQueryBuilder($filters);
|
||||
|
||||
// Логируем SQL запрос и параметры для отладки
|
||||
$query = $queryBuilder->getQuery();
|
||||
$sql = $query->getSQL();
|
||||
$params = [];
|
||||
foreach ($query->getParameters() as $param) {
|
||||
$params[$param->getName()] = $param->getValue();
|
||||
}
|
||||
|
||||
Logger::send([
|
||||
'method' => 'SpecialistMoreService::getPrices()',
|
||||
'action' => 'PriceList SQL params',
|
||||
'params' => $params
|
||||
], 'dev.log');
|
||||
|
||||
// Выполняем запрос
|
||||
$allPrices = $queryBuilder->getQuery()->getResult();
|
||||
|
||||
// Убираем дубликаты по комбинации kodoper + filial + schname
|
||||
$uniquePrices = [];
|
||||
$seenKeys = [];
|
||||
foreach ($allPrices as $price) {
|
||||
$kodoper = $price->getKodoper();
|
||||
$filial = $price->getFilial();
|
||||
$schname = $price->getSchname();
|
||||
// Создаем уникальный ключ
|
||||
$key = md5($kodoper . '|' . $filial . '|' . $schname);
|
||||
|
||||
if (!isset($seenKeys[$key])) {
|
||||
$seenKeys[$key] = true;
|
||||
$uniquePrices[] = $price;
|
||||
}
|
||||
}
|
||||
|
||||
$this->prices = $uniquePrices;
|
||||
|
||||
// Логируем детальную информацию о найденных записях
|
||||
$foundKodopers = [];
|
||||
$foundRecords = [];
|
||||
foreach ($this->prices as $price) {
|
||||
$kodoper = $price->getKodoper();
|
||||
$foundKodopers[] = $kodoper;
|
||||
$foundRecords[] = [
|
||||
'id' => $price->getId(),
|
||||
'kodoper' => $kodoper,
|
||||
'schname' => $price->getSchname(),
|
||||
'filial' => $price->getFilial(),
|
||||
'price' => $price->getPriceInfo()['price'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
Logger::send([
|
||||
'method' => 'SpecialistMoreService::getPrices()',
|
||||
'action' => 'PriceList query result',
|
||||
'kodoper' => $filters['kodoper'],
|
||||
'filial' => $filters['filial'] ?? 'not set',
|
||||
'found_records' => count($this->prices),
|
||||
'before_dedup' => count($allPrices),
|
||||
'found_kodopers' => $foundKodopers,
|
||||
'records_details' => $foundRecords
|
||||
], 'dev.log');
|
||||
}
|
||||
}
|
||||
|
||||
return $this->prices;
|
||||
}
|
||||
|
||||
public function minPrice(): ?PriceList
|
||||
{
|
||||
$minPriceRecord = null;
|
||||
$minPrice = PHP_INT_MAX;
|
||||
|
||||
foreach ($this->getPrices() as $record) {
|
||||
$currentPrice = $record->getPriceInfo()['price'];
|
||||
if ($currentPrice < $minPrice) {
|
||||
$minPrice = $currentPrice;
|
||||
$minPriceRecord = $record;
|
||||
}
|
||||
}
|
||||
|
||||
return $minPriceRecord;
|
||||
}
|
||||
|
||||
public function getReviews(): ?array
|
||||
{
|
||||
if (empty($this->reviews)) {
|
||||
$this->reviews = $this
|
||||
->reviewRepository
|
||||
->findBy(['specialistId' => $this->specialistId]);
|
||||
}
|
||||
|
||||
return $this->reviews;
|
||||
}
|
||||
|
||||
public function hasReviews(): bool
|
||||
{
|
||||
return !empty($this->getReviews());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\SpecialistView;
|
||||
use App\Repository\SpecialistViewRepository;
|
||||
use App\Repository\LocationViewRepository;
|
||||
Use App\Repository\ReviewRepository;
|
||||
use App\Service\SpecialistMoreService;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Knp\Bundle\PaginatorBundle\Pagination\SlidingPagination;
|
||||
use App\Repository\PriceListRepository;
|
||||
|
||||
class SpecialistService
|
||||
{
|
||||
public function __construct(
|
||||
private LocationViewRepository $locationViewRepository,
|
||||
private PriceListRepository $priceListRepository,
|
||||
private SpecialistViewRepository $specialistViewRepository,
|
||||
private ReviewRepository $reviewRepository,
|
||||
private PaginatorInterface $paginator
|
||||
) { }
|
||||
|
||||
public function listPaginated(
|
||||
array $filters = [],
|
||||
int $page = 1,
|
||||
int $limit = 10
|
||||
): SlidingPagination {
|
||||
$query = $this->specialistViewRepository
|
||||
->createFilteredQueryBuilder($filters)
|
||||
->getQuery()
|
||||
;
|
||||
|
||||
$paginatedSpecialists = $this->paginator->paginate($query, $page, $limit);
|
||||
|
||||
foreach ($paginatedSpecialists as $key => $specialist) {
|
||||
$specialist->addSpecialistMoreService(
|
||||
new SpecialistMoreService(
|
||||
$this->locationViewRepository,
|
||||
$this->reviewRepository,
|
||||
$this->priceListRepository,
|
||||
$filters['onlineMode'] ?? false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $paginatedSpecialists;
|
||||
}
|
||||
|
||||
public function list(array $filters = []): array
|
||||
{
|
||||
$specialists = $this->specialistViewRepository
|
||||
->createFilteredQueryBuilder($filters)
|
||||
->getQuery()
|
||||
->getResult()
|
||||
;
|
||||
|
||||
foreach ($specialists as $key => $specialist) {
|
||||
$specialist->addSpecialistMoreService(
|
||||
new SpecialistMoreService(
|
||||
$this->locationViewRepository,
|
||||
$this->reviewRepository,
|
||||
$this->priceListRepository,
|
||||
$filters['onlineMode'] ?? false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $specialists;
|
||||
}
|
||||
|
||||
public function show(array $filters = []): ?SpecialistView
|
||||
{
|
||||
$specialist = $this->specialistViewRepository
|
||||
->createFilteredQueryBuilder($filters)
|
||||
->setMaxResults(1)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
;
|
||||
|
||||
if ($specialist) {
|
||||
$specialist->addSpecialistMoreService(
|
||||
new SpecialistMoreService(
|
||||
$this->locationViewRepository,
|
||||
$this->reviewRepository,
|
||||
$this->priceListRepository,
|
||||
$filters['onlineMode'] ?? false
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $specialist;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Repository\UserRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\HttpFoundation\Session\Storage\Handler\RedisSessionHandler;
|
||||
|
||||
class UserCleanupService
|
||||
{
|
||||
private $userRepository;
|
||||
private $entityManager;
|
||||
private $logger;
|
||||
private $cookieLifetime;
|
||||
private $sessionHandler;
|
||||
|
||||
public function __construct(
|
||||
UserRepository $userRepository,
|
||||
EntityManagerInterface $entityManager,
|
||||
LoggerInterface $logger,
|
||||
array $sessionStorageOptions = [],
|
||||
$sessionHandler = null
|
||||
) {
|
||||
$this->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()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user