<?php
namespace App\Service;
use App\Dictionary\MemberSex;
use App\Entity\Account;
use App\Entity\Member;
use App\Entity\MemberProperty;
use App\Event\RedisEventManager;
use App\Event\User\AmateurUpdateEvent;
use App\Event\User\MemberCreatedEvent;
use App\Exception\AlreadyHasMemberException;
use App\Repository\MemberRepository;
use App\Response\EntityUpdated;
use App\Service\Property\MemberPropertyService;
use App\Service\User\AccountService;
use DateTime;
use Doctrine\ORM\Exception\ORMException;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr\Join;
use Doctrine\ORM\Tools\Pagination\Paginator;
use Frivol\Common\Dict\AccountStatus;
use JetBrains\PhpStorm\ArrayShape;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
class MemberService extends AbstractService implements EventDispatcherAwareInterface
{
/**
* @var MemberRepository
*/
protected $repo;
protected string $supportUsername = 'Support';
protected MemberPropertyService $propertyService;
protected RedisEventManager $redisEvents;
protected EventDispatcherInterface $dispatcher;
public function __construct(
MemberRepository $repository,
MemberPropertyService $propertyService,
RedisEventManager $redisEvents,
EventDispatcherInterface $dispatcher
) {
parent::__construct($repository);
$this->propertyService = $propertyService;
$this->redisEvents = $redisEvents;
$this->setEventDispatcher($dispatcher);
}
public function setEventDispatcher(EventDispatcherInterface $dispatcher): EventDispatcherAwareInterface
{
$this->dispatcher = $dispatcher;
return $this;
}
/**
* @throws AlreadyHasMemberException
*/
public function createMemberByAccount(Account $account, string $username = ''): Member
{
if ($account->hasMember()) {
throw new AlreadyHasMemberException(
"Can not create member. Account {$account->getId()} has already a member assigned."
);
}
if ('' === $username) {
$username = $this->generateUniqueUsername();
}
$member = new Member();
$member->setUsername($username);
$member->setAccount($account);
$account->setMember($member);
return $member;
}
protected function generateUniqueUsername(): string
{
$prefix = 'User';
$isUnique = false;
$username = $prefix . mt_rand(1, 999999);
while (!$isUnique) {
$isUnique = null === $this->getMemberByUsername($username, false);
if (!$isUnique) {
$username = $prefix . mt_rand(1, 999999);
}
}
return $username;
}
public function getMemberByUsername(string $username, bool $activeOnly): ?Member
{
$qb = $this->repo->getMemberByUsernameQueryBuilder($username, $activeOnly);
$result = $qb->getQuery()->getResult();
if (count($result)) {
return $result[0];
}
return null;
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function storeMember(Member $member): Member
{
$isNew = null === $member->getId();
$this->storeEntity($member);
if ($isNew) {
$event = new MemberCreatedEvent($member);
$this->getEventDispatcher()->dispatch($event);
} elseif (true === $member->getIsAmateur()) {
$event = new AmateurUpdateEvent($member);
$this->getEventDispatcher()->dispatch($event);
}
return $member;
}
public function getEventDispatcher(): EventDispatcherInterface
{
return $this->dispatcher;
}
public function getSupportMember(): ?Member
{
return $this->getMemberByUsername($this->getSupportUsername(), true);
}
public function getSupportUsername(): string
{
return $this->supportUsername;
}
public function setSupportUsername(string $username): self
{
$this->supportUsername = $username;
return $this;
}
public function getMemberById(int $id): ?Member
{
/**
* @var $result Member|null
*/
$result = $this->findById($id);
return $result;
}
public function getPublicIndex(int $page, int $limit, bool $activeOnly): Paginator
{
$qb = $this->repo->getIndexQueryBuilder($activeOnly);
$qb->setMaxResults($limit);
$qb->setFirstResult($page * $limit - $limit);
return new Paginator($qb->getQuery());
}
/**
* @throws \Doctrine\ORM\ORMException
* @throws \Doctrine\ORM\OptimisticLockException
*/
public function updateField(Member $member, ParameterBag $bag): EntityUpdated
{
$field = $bag->get('field');
$value = trim($bag->get('value'));
$updatedToAmateur = false;
$success = true;
switch ($field) {
case 'username':
$member->setUsername($value);
break;
case 'is_active':
$member->setIsActive($bag->getBoolean('value'));
break;
case 'is_amateur':
if (true === $bag->getBoolean('value') && false === $member->getIsAmateur()) {
$updatedToAmateur = true;
}
$member->setIsAmateur($bag->getBoolean('value'));
break;
case 'is_public_profile':
$member->setIsPublicProfile($bag->getBoolean('value'));
break;
case 'shall_hardcore':
$member->setShallHardcore((bool)$bag->getBoolean('value'));
break;
case 'hardcore_reason':
$member->setHardcoreReason($value);
break;
case 'sex':
$member->setSex($bag->getInt('value', MemberSex::MALE));
break;
case 'tax-country':
$member->setTaxCountry('' === $value ? null : $value);
break;
default:
$success = false;
break;
}
if ($success) {
$this->storeEntity($member);
if ($updatedToAmateur) {
// @TODO: Migrate to Doctrine Events
$this->propertyService->initFrontendApiKey($member);
}
}
return new EntityUpdated($success, $member->getId());
}
/**
* @throws \Exception
*/
public function applyValuesToEntity(ParameterBag $bag, Member $entity): Member
{
if ($bag->has('is_amateur')) {
$entity->setIsAmateur($bag->getBoolean('is_amateur', false));
}
if ($bag->has('is_active')) {
$entity->setIsActive($bag->getBoolean('is_active', true));
}
if ($bag->has('is_webcam_active')) {
$entity->setIsWebcamActive($bag->getBoolean('is_webcam_active', false));
}
if ($bag->has('is_public_profile')) {
$entity->setIsPublicProfile($bag->getBoolean('is_public_profile', true));
}
if ($bag->has('sex')) {
$entity->setSex($bag->getInt('sex', MemberSex::MALE));
}
if ($bag->has('username')) {
$entity->setUsername($bag->get('username'));
}
if ($bag->has('public_country')) {
$entity->setPublicCountry($bag->get('public_country', 'DE'));
}
if ($bag->has('public_region')) {
$entity->setPublicRegion($bag->get('public_region'));
}
if ($bag->has('public_zipcode')) {
if ('' === $bag->get('public_zipcode')) {
$entity->setPublicZipcode(null);
} else {
$entity->setPublicZipcode($bag->get('public_zipcode'));
}
}
if ($bag->has('date_of_birth')) {
$entity->setDateOfBirth(new DateTime($bag->get('date_of_birth')));
}
if ($bag->has('registration_date')) {
$entity->setRegistrationDate(new DateTime($bag->get('registration_date')));
}
return $entity;
}
#[ArrayShape([0 => 'bool', 1 => 'string'])]
public function changeUsername(Member $member, string $newUsername): array // TODO: Use DTO
{
if (mb_strlen($newUsername) > 32) {
return [false, 'Der Benutzername ist zu lang. Bitte auf 32 Zeichen beschränken.'];
}
if (1 !== preg_match('#^[a-zA-Z0-9\-]{2,32}$#', $newUsername)) {
return [false, 'Bitte verwende im Benutzernamen nur Buchstaben und Ziffern.'];
}
$usernameWithoutSpecials = str_replace('-', '', $newUsername);
if (strlen($usernameWithoutSpecials) <= 1) {
return [false, 'Bitte verwende im neuen Benutzernamen mehr Buchstaben oder Ziffern.'];
}
$newUsernameIsDefault = \Frivol\Common\Service\MemberService::qualifiesForChangeUsername($newUsername);
$oldUsername = $member->getUsername();
if ($newUsernameIsDefault || mb_strtolower($newUsername) === mb_strtolower($oldUsername)) {
return [false, 'Bitte wähle einen anderen Benutzername, der sich natürlicher anhört.'];
}
$hasDefaultUsername = \Frivol\Common\Service\MemberService::qualifiesForChangeUsername($member->getUsername());
if (!$hasDefaultUsername) {
$this->propertyService->deleteEntityByMemberAndName(
$member,
AccountService::PROPERTY_ACCOUNT_USERNAME_CHANGE_PERMITTED
);
return [false, 'Dein Benutzername kann nicht mehr geändert werden.'];
}
$property = $this->propertyService->getPropertyForMember(
$member,
AccountService::PROPERTY_ACCOUNT_USERNAME_CHANGE_PERMITTED
);
// If there is no property, the username has already been changed.
if (!$property instanceof MemberProperty) {
return [false, 'Dein Benutzername wurde schon mal geändert und kann momentan nicht mehr geändert werden.'];
}
try {
$member->setUsername($newUsername);
$this->storeEntity($member);
$this->propertyService->deleteEntityByMemberAndName(
$member,
AccountService::PROPERTY_ACCOUNT_USERNAME_CHANGE_PERMITTED
);
// Publishing name changed event currently not required
// $this->redisEvents->publishServerEvent(new UsernameChangedEvent($member->getId(), $oldUsername, $newUsername));
return [true, 'Danke! Benutzername wurde geändert.'];
} catch (\Exception|\Doctrine\ORM\ORMException|ORMException $e) {
if (false !== stripos($e->getMessage(), 'duplicate entry')) {
return [false, 'Der gewählte Benutzername ist bereits vergeben.'];
}
return [false, 'Es ist ein Fehler aufgetreten. Bitte probiere es nochmal.'];
}
}
public function getMembersAmateurStatus(array $memberIds, bool $activeOnly = true): array
{
$qb = $this->repo->createQueryBuilder('m');
$qb = $qb->select('m.id, m.is_amateur AS isAmateur')
->where('m.id IN (:memberIds)')
->andWhere('m.is_active = :activeOnly')
->setParameters([
'memberIds' => $memberIds,
'activeOnly' => ($activeOnly ? 1 : 0),
])->setMaxResults(1000);
if ($activeOnly) {
$qb->join(Account::class, 'a', Join::WITH, 'a.member = m.id')
->andWhere('a.status = ' . AccountStatus::ACTIVE);
}
$arrayResult = $qb->getQuery()->getResult(Query::HYDRATE_ARRAY);
if (empty($arrayResult)) {
return [];
}
$results = [];
foreach ($arrayResult as $row) {
$results[$row['id']] = $row['isAmateur'];
}
return $results;
}
public function getInaccessible(): array
{
return $this->repo->findInaccessible()->getResult();
}
public function getAccessibleMemberIds(): array
{
return $this->repo->findAccessibleMemberIds();
}
public function amateursWithoutFrontendApiKey(): array
{
return $this->repo->findAmateursWithoutFrontendApiKey();
}
}