Files
2025-08-02 16:30:27 +02:00

581 lines
19 KiB
PHP

<?php
namespace KupShop\GraphQLBundle\ApiAdmin\Util;
use KupShop\CatalogBundle\Query\Search;
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserBonusPointCollection;
use KupShop\GraphQLBundle\ApiAdmin\Types\Collection\UserCollection;
use KupShop\GraphQLBundle\ApiAdmin\Types\Parameters;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Enum\BonusPointStatus;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\AddressInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserAddressInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserBonusPointFilterInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserGroupsRelationInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Input\UserNewsletterInput;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserAddressMutateResponse;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\Response\UserMutateResponse;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\User;
use KupShop\GraphQLBundle\ApiAdmin\Types\User\UserBonusPoint;
use KupShop\GraphQLBundle\ApiShared\ApiUtil;
use KupShop\GraphQLBundle\ApiShared\Util\ParametersAssembler;
use KupShop\GraphQLBundle\Exception\GraphQLNotFoundException;
use KupShop\GraphQLBundle\Exception\GraphQLValidationException;
use KupShop\KupShopBundle\Context\CountryContext;
use KupShop\KupShopBundle\Email\PasswordResetAdminEmail;
use KupShop\KupShopBundle\Util\Contexts;
use KupShop\KupShopBundle\Util\Database\QueryHint;
use KupShop\KupShopBundle\Util\Mail\EmailCheck;
use KupShop\KupShopBundle\Util\ObjectUtil;
use KupShop\PricelistBundle\Util\PriceListWorker;
use KupShop\UserBundle\Security\LegacyPasswordEncoder;
use KupShop\UserBundle\Util\UserConsent;
use Query\Operator;
use Query\QueryBuilder;
use Symfony\Contracts\Service\Attribute\Required;
class UserUtil
{
/**
* @var ParametersAssembler
*/
private $parametersAssembler;
#[Required]
public PasswordResetAdminEmail $passwordResetEmail;
public function __construct(
private readonly EmailCheck $emailCheck,
private readonly UserConsent $userConsent,
private readonly LegacyPasswordEncoder $passwordEncoder,
private readonly ?PriceListWorker $priceListWorker = null,
private readonly ?UserAddressesUtil $userAddressesUtil = null,
) {
}
public function getUserCollection(int $offset, int $limit, ?Parameters $userSort, ?Parameters $userFilter): UserCollection
{
$filterSpec = $this->createUserCollectionFilter($offset, $limit, $userSort, $userFilter);
$users = $this->getUsers($filterSpec, $totalCount);
return (new UserCollection($users))
->setItemsTotalCount($totalCount)
->setLimit($limit)
->setOffset($offset);
}
public function getUser(?int $id, ?string $email, ?string $cartId = null): ?User
{
if (!$id && !$email && !$cartId) {
throw new GraphQLValidationException('Cannot query `user` without id or email or cartId provided!');
}
$users = $this->getUsers(Operator::equals(array_filter(['u.id' => $id, 'u.email' => $email, 'u.user_key' => $cartId]), 'OR'));
if (!($user = reset($users))) {
throw new GraphQLNotFoundException('User not found!');
}
return $user;
}
/**
* @return User[]
*/
public function getUsers(callable $filterSpec, ?int &$totalCount = null): array
{
$useTotalCount = count(func_get_args()) > 1;
$qb = sqlQueryBuilder()
->addCalcRows()
->select('u.*')
->from('users', 'u')
->andWhere($filterSpec)
->groupBy('u.id');
$users = [];
foreach ($qb->execute() as $item) {
$user = new \User();
$user->loadData($item);
$user->fetchAddresses($item);
$users[$item['id']] = $user;
}
if ($useTotalCount) {
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
}
$this->processUsersMultiFetches($users);
return ApiUtil::wrapItems($users, User::class);
}
public function getUsersBonusPoints(int $offset, int $limit, ?Parameters $sort, ?UserBonusPointFilterInput $filter): UserBonusPointCollection
{
$qb = sqlQueryBuilder()
->addCalcRows()
->select('bp.*')
->from('bonus_points', 'bp')
->andWhere(ApiUtil::getLimitSpec($offset, $limit))
->andWhere(ApiUtil::getSortSpec($sort, 'bp'));
if ($filter?->id) {
$qb->andWhere(Operator::inIntArray($filter->id, 'bp.id'));
}
if ($filter?->userId) {
$qb->andWhere(Operator::inIntArray($filter->userId, 'bp.id_user'));
}
if ($filter?->orderId) {
$qb->andWhere(Operator::inIntArray($filter->orderId, 'bp.id_order'));
}
if ($filter?->status) {
$qb->andWhere(Operator::inStringArray(array_map(fn (BonusPointStatus $status) => $status->value, $filter->status), 'bp.status'));
}
if ($filter?->dateCreated) {
$qb->andWhere(ApiUtil::getDateTimeFilter($filter->dateCreated, 'bp.date_created'));
}
$data = $qb->execute();
$totalCount = (int) sqlFetchAssoc(sqlQuery('SELECT FOUND_ROWS() as total_count'))['total_count'];
return (new UserBonusPointCollection(
ApiUtil::wrapItems($data, UserBonusPoint::class))
)
->setOffset($offset)
->setLimit($limit)
->setItemsTotalCount($totalCount);
}
public function addOrRemoveUserGroupsRelations(UserGroupsRelationInput $input): UserMutateResponse
{
// zkontroluju, ze uzivatel existuje
$this->validateUser($input->userId);
// zkontroluju, ze vsechny uzivatelske skupiny existuji
foreach ($input->userGroupIds as $group) {
$this->validateUserGroup($group);
}
if ($input->overwrite) {
sqlQueryBuilder()
->delete('users_groups_relations')
->where(Operator::equals(['id_user' => $input->userId]))
->execute();
}
foreach ($input->userGroupIds as $group) {
sqlQueryBuilder()
->insert('users_groups_relations')
->directValues([
'id_user' => $input->userId,
'id_group' => $group,
])
->onDuplicateKeyUpdate(['id_group' => $group, 'id_user' => $input->userId])
->execute();
}
return new UserMutateResponse(true, $this->getUpdatedUser($input->userId));
}
public function createOrUpdateUser(UserInput $input): UserMutateResponse
{
if (empty($input->id) && !ObjectUtil::isPropertyInitialized($input, 'email')) {
throw new GraphQLValidationException('Field "email" is required when creating user!');
}
// zkontroloval, ze uzivatel se zadanym ID existuje
if (isset($input->id)) {
$userExists = sqlQueryBuilder()
->select('id')
->from('users')
->where(Operator::equals(['id' => $input->id]))
->execute()->fetchOne();
if (!$userExists) {
throw new GraphQLNotFoundException(sprintf('User with ID "%s" does not exists!', $input->id));
}
}
// zkontroluju mail
if (ObjectUtil::isPropertyInitialized($input, 'email')) {
$this->validateUserEmail($input->email, !empty($input->id) ? $input->id : null);
}
// zkontroluju, ze zadavam existujici cenik
if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
$this->validateUserPriceList($input->priceListId);
}
if (ObjectUtil::isPropertyInitialized($input, 'password')) {
$this->validateUserPassword($input->password);
}
$userData = $this->getUserData($input);
$isNewUser = false;
$userId = !empty($input->id) ? $input->id : null;
// vytvoreni zakaznika
if (!$userId) {
$defaults = [
'figure' => 'Y',
'date_reg' => (new \DateTime())->format('Y-m-d H:i:s'),
];
$isNewUser = true;
$insert = array_merge($userData, $defaults);
$userId = sqlGetConnection()->transactional(function () use ($insert) {
sqlQueryBuilder()
->insert('users')
->directValues($insert)
->execute();
return (int) sqlInsertId();
});
} elseif (!empty($userData)) {
// aktualizace zakaznika
sqlQueryBuilder()
->update('users')
->directValues($userData)
->where(Operator::equals(['id' => $userId]))
->execute();
}
if (ObjectUtil::isPropertyInitialized($input, 'data')) {
ApiUtil::updateObjectCustomData('users', $userId, $input->data, 'custom_data');
}
if (ObjectUtil::isPropertyInitialized($input, 'newsletter')) {
$this->updateUserNewsletter($userId, $isNewUser, $input->newsletter);
}
if ($input->sendPasswordEmail) {
$this->sendPasswordResetEmail($userId);
}
return new UserMutateResponse(true, $this->getUpdatedUser($userId));
}
protected function getUserData(UserInput $input): array
{
$data = [];
if (ObjectUtil::isPropertyInitialized($input, 'email')) {
$data['email'] = $input->email;
}
if (ObjectUtil::isPropertyInitialized($input, 'password')) {
$data['passw'] = $input->password ? $this->passwordEncoder->hash($input->password) : '';
}
if (ObjectUtil::isPropertyInitialized($input, 'ico')) {
$data['ico'] = $input->ico;
}
if (ObjectUtil::isPropertyInitialized($input, 'dic')) {
$data['dic'] = $input->dic;
}
if (ObjectUtil::isPropertyInitialized($input, 'isActive')) {
$data['figure'] = $input->isActive ? 'Y' : 'N';
}
if (ObjectUtil::isPropertyInitialized($input, 'priceListId')) {
$data['id_pricelist'] = $input->priceListId;
}
if (ObjectUtil::isPropertyInitialized($input, 'invoiceAddress')) {
$address = $this->getUserAddress($input->invoiceAddress);
if (!empty($address)) {
$data = array_merge($data, $address);
}
}
if (ObjectUtil::isPropertyInitialized($input, 'deliveryAddress')) {
$address = $this->getUserAddress($input->deliveryAddress);
foreach ($address as $key => $value) {
if (in_array($key, ['ico', 'dic'])) {
continue;
}
$data['delivery_'.$key] = $value;
}
}
return $data;
}
protected function updateUserNewsletter(int $userId, bool $isNewUser, ?UserNewsletterInput $newsletterInput): void
{
if (!$newsletterInput) {
return;
}
if (ObjectUtil::isPropertyInitialized($newsletterInput, 'isSubscribed') && $newsletterInput->isSubscribed !== null) {
$this->userConsent->updateNewsletter(
userId: $userId,
newsletter: $newsletterInput->isSubscribed ? 'Y' : 'N',
new_user: $isNewUser,
confirmed: true
);
}
}
public function getUpdatedUser(int $userId): User
{
$users = QueryHint::withRouteToMaster(fn () => $this->getUsers(Operator::equals(['u.id' => $userId])));
return reset($users);
}
protected function getUserAddress(AddressInput|UserAddressInput $input): array
{
// validovat zemi
if (ObjectUtil::isPropertyInitialized($input, 'country')) {
$countries = Contexts::get(CountryContext::class)->getAll();
if (empty($countries[$input->country])) {
throw new GraphQLValidationException(sprintf('Country "%s" does not exist!', $input->country));
}
}
return (array) $input;
}
protected function prepareUserDeliveryAddress(array $address): array
{
$userDeliveryAddress = [];
if (!$this->checkUser($address['userId']) ?? false) {
throw new GraphQLValidationException(sprintf('User with ID "%s" does not exist!', $address['userId']));
}
$address = $this->renameAddressFields($address);
foreach ($address as $key => $value) {
if (in_array($key, ['id', 'id_user'])) {
$userDeliveryAddress[$key] = $value;
continue;
}
$userDeliveryAddress['delivery_'.$key] = $value;
}
return $userDeliveryAddress;
}
private function renameAddressFields(array $address)
{
$address['id_user'] = $address['userId'];
$address['custom_address'] = $address['customAddress'] ?? '';
unset($address['userId']);
unset($address['customAddress']);
return $address;
}
private function checkUser(int $userId): ?int
{
return sqlQueryBuilder()
->select('id')
->from('users')
->where(Operator::equals(['id' => $userId]))
->execute()->fetchOne() ?: null;
}
private function checkCurrency(string $currency): ?string
{
return sqlQueryBuilder()
->select('id')
->from('currencies')
->where(Operator::equals(['id' => $currency]))
->execute()->fetchOne() ?: null;
}
protected function validateUserPassword(?string $password): void
{
if ($password === null) {
return;
}
if (strlen($password) < 6) {
throw new GraphQLValidationException(
message: 'Password must be at least 6 characters long.',
extensions: ['field' => 'password'],
);
}
}
protected function validateUserEmail(?string $email, ?int $userId = null): void
{
if (empty($email)) {
throw new GraphQLValidationException('Field "email" cannot be empty!');
}
$spec = [
Operator::equals(['email' => $email]),
];
if ($userId) {
$spec[] = Operator::not(
Operator::equals(['id' => $userId])
);
}
// kontrola, zda se nesnazim nastavit email, ktery uz ma jiny zakaznik
$emailExists = sqlQueryBuilder()
->select('id')
->from('users')
->where(Operator::andX($spec))
->execute()->fetchOne();
if ($emailExists) {
throw new GraphQLValidationException(
sprintf('Email "%s" is already set up for another user!', $email)
);
}
if (!$this->emailCheck->isEmailDomainValid($email)) {
throw new GraphQLValidationException(sprintf('Email "%s" is not valid!', $email));
}
}
protected function validateUser(int $userId): void
{
$userExists = sqlQueryBuilder()
->select('id')
->from('users')
->where(Operator::equals(['id' => $userId]))
->sendToMaster()->execute()->fetchOne();
if (!$userExists) {
throw new GraphQLNotFoundException(sprintf(
'User with ID "%s" does not exist!', $userId)
);
}
}
protected function validateUserGroup(int $userGroup): void
{
$userGroupExists = sqlQueryBuilder()
->select('id')
->from('users_groups')
->where(Operator::equals(['id' => $userGroup]))
->execute()->fetchOne();
if (!$userGroupExists) {
throw new GraphQLValidationException(
sprintf('User group with ID "%s" does not exist!', $userGroup)
);
}
}
protected function validateUserPriceList(?int $priceListId): void
{
if (!$this->priceListWorker) {
return;
}
if ($priceListId !== null && !$this->priceListWorker->getPriceList($priceListId)) {
throw new GraphQLValidationException(
sprintf('Price list with ID "%s" does not exists!', $priceListId)
);
}
}
protected function createUserCollectionFilter(int $offset, int $limit, ?Parameters $sort, ?Parameters $filter): callable
{
$phone = $filter?->get('phone');
if (!empty($phone)) {
$filter->set('phone', Operator::like(['u.phone' => '%'.$phone.'%']));
} else {
$filter?->remove('phone');
}
if (!empty($filter?->get('name'))) {
$filter->set('name', Search::searchFields($filter->get('name'), [
['field' => 'u.name', 'match' => 'both'],
['field' => 'u.surname', 'match' => 'both'],
]));
} else {
$filter?->remove('name');
}
return function (QueryBuilder $qb) use ($offset, $limit, $sort, $filter) {
$this->parametersAssembler->assemblyInput($filter, $sort, $offset, $limit, $qb);
};
}
protected function saveDeliveryAddress(array $address): int
{
return sqlGetConnection()->transactional(function () use ($address) {
$qb = sqlQueryBuilder();
if ($id = ($address['id'] ?? false)) {
unset($address['id']);
$qb->update('users_addresses')
->directValues($address)
->where(Operator::equals(['id' => $id]))
->execute();
return $id;
}
$qb->insert('users_addresses')
->directValues($address)
->execute();
return sqlInsertId();
});
}
public function createOrUpdateUserAddress(UserAddressInput $input): UserAddressMutateResponse
{
$address = $this->getUserAddress($input);
if (isset($address['id'])) {
$addressExists = sqlQueryBuilder()
->select('id')
->from('users_addresses')
->where(Operator::equals(['id' => $address['id']]))
->execute()->fetchOne();
if (!$addressExists) {
throw new GraphQLNotFoundException(sprintf('Address with ID "%s" does not exists!', $address['id']));
}
}
$address = $this->prepareUserDeliveryAddress($address);
return new UserAddressMutateResponse(true, $this->saveDeliveryAddress($address));
}
protected function processUsersMultiFetches(array &$users): void
{
}
/**
* @required
*/
public function setParametersAssembler(ParametersAssembler $parametersAssembler): void
{
$this->parametersAssembler = $parametersAssembler;
}
public function sendPasswordResetEmail(int $userId): void
{
$user = QueryHint::withRouteToMaster(fn () => \User::createFromId($userId));
$this->passwordResetEmail->setUser($user);
$email = $this->passwordResetEmail->getEmail();
$email['to'] = $user->email;
$this->passwordResetEmail->sendEmail($email);
}
}