<?php declare(strict_types=1);
namespace App\Entity;
use App\Contract\Payments\PaymentUserInterface;
use App\Entity\Message\MessageBid;
use App\Model\UserLoginInfoInterface;
use DateTime;
use DateTimeZone;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\Mapping as ORM;
use Gedmo\Mapping\Annotation as Gedmo;
use JMS\Serializer\Annotation as JMS;
use RuntimeException;
use Symfony\Component\Security\Core\User\EquatableInterface;
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Contracts\Service\ResetInterface;
use function count;
/**
* @ORM\Entity(repositoryClass="App\Repository\UserRepository")
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="cache_user")
* @JMS\ReadOnlyProperty()
* @JMS\ExclusionPolicy(policy="all")
*/
class User implements UserInterface, UserLoginInfoInterface, PaymentUserInterface, PasswordAuthenticatedUserInterface, ResetInterface, EquatableInterface
{
public const ROLE_VERIFIED = 'ROLE_VERIFIED';
public const ROLE_HAS_MEMBERSHIP = 'ROLE_HAS_MEMBERSHIP';
private const PREVIOUS_EMAILS = 'previous_emails';
private const NOTIFIED_CC_EXPIRATION = 'notified_cc';
/**
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer", options={"unsigned": true})
*/
private ?int $id = null;
/**
* @ORM\Column(type="string", unique=true, length=255)
*/
private string $email = '';
/**
* @ORM\Column(type="string", length=400)
*/
private string $nickname = '';
/**
* @ORM\Column(type="string", length=255)
*/
private string $password = '';
private ?string $plainPassword = null;
/**
* @ORM\Column(type="boolean")
*/
private bool $locked = false;
/**
* @ORM\Column(type="boolean")
*/
private bool $verified = false;
private bool $justVerified = false;
/**
* @ORM\Column(type="boolean")
*/
private bool $verificationLock = false;
/**
* @ORM\Column(type="boolean")
*/
private bool $agreedNewsletter = false;
/**
* @ORM\Column(type="string", nullable=true)
*/
private ?string $lastIp;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private ?DateTime $lastLogin;
/**
* @ORM\Column(type="datetime")
* @Gedmo\Timestampable(on="update")
*/
private DateTime $lastModified;
/**
* @ORM\Column(type="datetime")
*/
private DateTime $createdAt;
/**
* @ORM\Column(type="integer")
*/
private int $credits = 0;
/**
* @var DealBid[]|Collection<int, DealBid>
* @ORM\OneToMany(targetEntity="DealBid", mappedBy="user")
*/
private Collection $dealBids;
/**
* @var ProjectBid[]|Collection<int, ProjectBid>
* @ORM\OneToMany(targetEntity="ProjectBid", mappedBy="user")
*/
private Collection $projectBids;
/**
* @var ShopVideoBid[]|Collection<int, ShopVideoBid>
* @ORM\OneToMany(targetEntity="ShopVideoBid", mappedBy="user")
*/
private Collection $shopVideoBids;
/**
* @var Collection<int, MessageBid>
* @ORM\OneToMany(targetEntity="App\Entity\Message\MessageBid", mappedBy="user")
*/
private Collection $messageBids;
/**
* @var Collection<int, Favorite>
* @ORM\OneToMany(targetEntity="App\Entity\Favorite", mappedBy="user")
*/
private Collection $favorites;
/**
* @var Referral[]|Collection<int, Referral>
* @ORM\OneToMany(targetEntity="App\Entity\Referral", mappedBy="user", cascade={"persist"})
*/
private Collection $referrals;
/**
* @var CreditsMembership[]|Collection<int, CreditsMembership>
* @ORM\OneToMany(targetEntity="CreditsMembership", mappedBy="user", cascade={"refresh"})
* @ORM\Cache(usage="NONSTRICT_READ_WRITE", region="cache_user")
*/
private Collection $creditsMemberships;
private ?CreditsMembership $activeCreditsMembership = null;
/**
* @ORM\Column(type="string", length=255)
*/
private string $baseTransactionId = '';
/**
* @var string[]
*/
private array $defaultRoles = ['ROLE_USER'];
/**
* @var string[]
*/
private array $roles;
/**
* @var array<mixed>
* @ORM\Column(type="json")
*/
private array $metaInfo = [];
public function __construct()
{
$this->createdAt = new DateTime();
$this->lastLogin = new DateTime();
$this->dealBids = new ArrayCollection();
$this->projectBids = new ArrayCollection();
$this->shopVideoBids = new ArrayCollection();
$this->messageBids = new ArrayCollection();
$this->referrals = new ArrayCollection();
$this->creditsMemberships = new ArrayCollection();
$this->favorites = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function setId(int $id): void
{
$this->id = $id;
}
public function getEmail(): ?string
{
return $this->email;
}
public function setEmail(?string $email): self
{
if (null === $email) {
$email = '';
}
if ('' !== $email && '' !== $this->email) {
if (!isset($this->metaInfo[self::PREVIOUS_EMAILS]) || !is_array($this->metaInfo[self::PREVIOUS_EMAILS])) {
$this->metaInfo[self::PREVIOUS_EMAILS] = [];
}
$this->metaInfo[self::PREVIOUS_EMAILS][] = $this->email;
}
$this->email = $email;
return $this;
}
public function getNickname(): string
{
return $this->nickname;
}
public function setNickname(?string $nickname): User
{
if (null === $nickname) {
$nickname = '';
}
$this->nickname = $nickname;
return $this;
}
/**
* @return string[]
*/
public function getRoles(): array
{
if (isset($this->roles)) {
return $this->roles;
}
$roles = $this->defaultRoles;
if ($this->isVerified()) {
$roles[] = self::ROLE_VERIFIED;
}
if (null !== $this->getActiveCreditsMembership()) {
$roles[] = self::ROLE_HAS_MEMBERSHIP;
}
return $this->roles = $roles;
}
public function resetRoles(): void
{
unset($this->roles);
}
public function getSalt(): string
{
return '';
}
public function getUsername(): string
{
return $this->email;
}
public function getUserIdentifier(): string
{
return $this->email;
}
public function eraseCredentials(): void
{
$this->plainPassword = null;
}
public function getPassword(): string
{
return $this->password;
}
public function isEqualTo(UserInterface $user): bool
{
if (!$user instanceof self) {
return false;
}
if ($this->getPassword() !== $user->getPassword()) {
return false;
}
if ($this->getSalt() !== $user->getSalt()) {
return false;
}
$currentRoles = $this->getRoles();
$newRoles = $user->getRoles();
$rolesChanged = count($currentRoles) !== count($newRoles) || count($currentRoles) !== count(array_intersect($currentRoles, $newRoles));
if ($rolesChanged) {
return false;
}
if ($this->getUserIdentifier() !== $user->getUserIdentifier()) {
return false;
}
if ($this->isLocked() !== $user->isLocked()) {
return false;
}
return $this->isVerificationLock() === $user->isVerificationLock();
}
public function setPassword(string $password): User
{
$this->password = $password;
return $this;
}
public function getPlainPassword(): ?string
{
return $this->plainPassword;
}
public function setPlainPassword(?string $plainPassword): User
{
$this->plainPassword = $plainPassword;
return $this;
}
public function isLocked(): bool
{
return $this->locked;
}
public function setLocked(bool $locked): User
{
$this->locked = $locked;
return $this;
}
public function isVerified(): bool
{
return $this->verified;
}
public function setVerified(bool $verified): User
{
if (!$this->verified && $verified) {
$this->justVerified = true;
}
$this->verified = $verified;
return $this;
}
public function isJustVerified(): bool
{
return $this->justVerified;
}
public function isVerificationLock(): bool
{
return $this->verificationLock;
}
public function setVerificationLock(bool $verificationLock): User
{
$this->verificationLock = $verificationLock;
return $this;
}
public function isAgreedNewsletter(): bool
{
return $this->agreedNewsletter;
}
public function setAgreedNewsletter(bool $agreedNewsletter): User
{
$this->agreedNewsletter = $agreedNewsletter;
return $this;
}
public function getLastIp(): ?string
{
return $this->lastIp;
}
public function setLastIp(?string $lastIp): User
{
$this->lastIp = $lastIp;
return $this;
}
public function getLastLogin(): ?DateTime
{
return $this->lastLogin;
}
public function setLastLogin(?DateTime $lastLogin): User
{
$this->lastLogin = $lastLogin;
return $this;
}
public function getLastModified(): DateTime
{
return $this->lastModified;
}
public function setLastModified(DateTime $lastModified): User
{
$this->lastModified = $lastModified;
return $this;
}
public function getCreatedAt(): DateTime
{
return $this->createdAt;
}
public function setCreatedAt(DateTime $createdAt): User
{
$this->createdAt = $createdAt;
return $this;
}
public function getCredits(): int
{
return $this->credits;
}
public function getCreditsFloat(): float
{
return $this->credits / 100;
}
// Only getter. Use UserRepository::addCredits
// private function setCredits(int $credits): void
// {
// $this->credits = $credits;
// }
/**
* @return DealBid[]|Collection<int, DealBid>
*/
public function getDealBids(): Collection
{
return $this->dealBids;
}
public function addDealBid(DealBid $bid): void
{
if ($this->dealBids->contains($bid)) {
return;
}
$this->dealBids->add($bid);
}
/**
* @return ProjectBid[]|Collection<int, ProjectBid>
*/
public function getProjectBids(): Collection
{
return $this->projectBids;
}
public function addProjectBid(ProjectBid $bid): void
{
if ($this->projectBids->contains($bid)) {
return;
}
$this->projectBids->add($bid);
}
/**
* @return ShopVideoBid[]|Collection<int, ShopVideoBid>
*/
public function getShopVideoBids()
{
return $this->shopVideoBids;
}
public function addShopVideoBid(ShopVideoBid $bid): void
{
if ($this->shopVideoBids->contains($bid)) {
return;
}
$this->shopVideoBids->add($bid);
}
/**
* @return Collection<int, MessageBid>
*/
public function getMessageBids(): Collection
{
return $this->messageBids;
}
public function addMessageBid(MessageBid $bid): void
{
if ($this->messageBids->contains($bid)) {
return;
}
$this->messageBids->add($bid);
}
/**
* @return Collection<int, Favorite>
*/
public function getFavorites(): Collection
{
return $this->favorites;
}
public function addFavorite(Favorite $favorite): void
{
if ($this->favorites->contains($favorite)) {
return;
}
$this->favorites->add($favorite);
}
public function removeFavorite(Favorite $favorite): void
{
$this->favorites->removeElement($favorite);
}
public function getReferral(): ?Referral
{
$referrals = $this->referrals->filter(static function (Referral $referral): bool {
return !$referral->isCancelled();
});
if (0 === count($referrals)) {
return null;
}
return $referrals->first() !== false ? $referrals->first() : null;
}
public function addReferral(User $referredBy): Referral
{
if (null !== $this->getReferral()) {
throw new RuntimeException('Referral for user ' . $this->getId() . ' already exists.');
}
$referral = new Referral($this, $referredBy);
$this->referrals->add($referral);
return $referral;
}
/**
* @return Collection<int, CreditsMembership>
*/
public function getCreditsMemberships(): Collection
{
return $this->creditsMemberships;
}
/**
* @throws RuntimeException
*/
public function getActiveCreditsMembership(): ?CreditsMembership
{
if (null === $this->activeCreditsMembership) {
$nowUTC = new DateTime('now', new DateTimeZone('UTC'));
// Because CreditsMemberships are hydrated they have a local default timezone set.
// We need to grab a time string of UTC and apply local timezone to properly compare dates.
$nowDefaultTimezone = new DateTime($nowUTC->format('Y-m-d H:i:s'));
// Force PersistentCollection to load. This will allow to a greater cache hits, as it does not use
// time-sensitive query. If we'd have a problem with hydrating CreditMemberships (> 50? 100?) then its
// luxury problem, and should be solved by shifting old statistical data to statistical database.
$this->creditsMemberships->first();
$activeCreditsMembership = $this->creditsMemberships->matching(
Criteria::create()
->where(Criteria::expr()->lte('lastTransactionAt', $nowDefaultTimezone))
->andWhere(Criteria::expr()->neq('lastSuccessfulTransactionAt', null))
->andWhere(Criteria::expr()->lte('lastSuccessfulTransactionAt', $nowDefaultTimezone))
// need to leave a gap for users who are currently browsing a website and should be re-billed
->andWhere(Criteria::expr()->gte('nextTransactionAt', (clone $nowDefaultTimezone)->modify('-10 minutes')))
->andWhere(Criteria::expr()->eq('cancelled', false))
);
if ($activeCreditsMembership->count() > 1) {
throw new RuntimeException('User has too many active credits loader.');
}
if ($activeCreditsMembership->count() === 1 && false !== $activeCreditsMembership->first()) {
$this->activeCreditsMembership = $activeCreditsMembership->first();
}
}
return $this->activeCreditsMembership;
}
public function isActiveCreditsMembershipConfirmed(): bool
{
$activeCreditsMembership = $this->getActiveCreditsMembership();
if (null === $activeCreditsMembership || null === $activeCreditsMembership->getLastSuccessfulTransactionAt()) {
return false;
}
return $activeCreditsMembership->isConfirmedActiveCreditsMembership();
}
public function getBaseTransactionId(): ?string
{
return $this->baseTransactionId === '' ? null : $this->baseTransactionId;
}
public function setBaseTransactionId(string $baseTransactionId): void
{
$this->baseTransactionId = $baseTransactionId;
}
public function getTrackingHash(): ?string
{
if (isset($this->metaInfo['tracking_hash'])) {
if (!is_string($this->metaInfo['tracking_hash'])) {
unset($this->metaInfo['tracking_hash']);
return null;
}
return $this->metaInfo['tracking_hash'];
}
return null;
}
public function setTrackingHash(string $trackingHash): void
{
if ($trackingHash === '') {
return;
}
$this->metaInfo['tracking_hash'] = $trackingHash;
}
public function setNotifyProject(bool $notifyProject): void
{
$this->metaInfo['notify_project'] = $notifyProject;
}
public function isNotifyProject(): bool
{
return !isset($this->metaInfo['notify_project']) || !is_bool($this->metaInfo['notify_project']) ? true : $this->metaInfo['notify_project'];
}
public function setNotifyDeal(bool $notifyDeal): void
{
$this->metaInfo['notify_deal'] = $notifyDeal;
}
public function isNotifyDeal(): bool
{
return !isset($this->metaInfo['notify_deal']) || !is_bool($this->metaInfo['notify_deal']) ? true : $this->metaInfo['notify_deal'];
}
public function setNotifyMessage(bool $notifyMessage): void
{
$this->metaInfo['notify_message'] = $notifyMessage;
}
public function isNotifyMessage(): bool
{
return !isset($this->metaInfo['notify_message']) || !is_bool($this->metaInfo['notify_message']) ? true : $this->metaInfo['notify_message'];
}
/**
* @param Project|Deal $proposal
*/
public function shouldNotifyProposal(Proposal $proposal): bool
{
if ($proposal instanceof Project && $this->isNotifyProject()) {
return true;
}
return $proposal instanceof Deal && $this->isNotifyDeal();
}
public function getPreferredTheme(): ?string
{
if (!isset($this->metaInfo['theme']) || !is_string($this->metaInfo['theme'])) {
return null;
}
return $this->metaInfo['theme'];
}
public function setPreferredTheme(string $theme): void
{
if ($theme === '') {
return;
}
$this->metaInfo['theme'] = $theme;
}
/**
* @return mixed[]
*/
public function getMetaInfo(): array
{
return $this->metaInfo;
}
/**
* @param mixed[] $metaInfo
*/
public function setMetaInfo(array $metaInfo): User
{
$this->metaInfo = $metaInfo;
return $this;
}
/**
* @return array<int, string>
*/
public function getPreviousEmails(): array
{
if (!isset($this->metaInfo[self::PREVIOUS_EMAILS]) || !is_array($this->metaInfo[self::PREVIOUS_EMAILS])) {
return [];
}
return $this->metaInfo[self::PREVIOUS_EMAILS];
}
public function getNotifiedExpiration(int $transactionId): bool
{
if (!isset($this->metaInfo[self::NOTIFIED_CC_EXPIRATION])
|| !is_array($this->metaInfo[self::NOTIFIED_CC_EXPIRATION])
|| !isset($this->metaInfo[self::NOTIFIED_CC_EXPIRATION][$transactionId])
|| !is_bool($this->metaInfo[self::NOTIFIED_CC_EXPIRATION][$transactionId])
) {
return false;
}
return $this->metaInfo[self::NOTIFIED_CC_EXPIRATION][$transactionId];
}
public function setEmailNotifiedExpiration(int $transactionId): void
{
if (!isset($this->metaInfo[self::NOTIFIED_CC_EXPIRATION])
|| !is_array($this->metaInfo[self::NOTIFIED_CC_EXPIRATION])
) {
$this->metaInfo[self::NOTIFIED_CC_EXPIRATION] = [];
}
$this->metaInfo[self::NOTIFIED_CC_EXPIRATION][$transactionId] = true;
}
public function __toString(): string
{
return $this->nickname !== '' ? $this->nickname : 'user-' . $this->getId();
}
/**
* @return mixed[]
*/
public function __serialize(): array
{
if (!isset($this->roles)) {
$this->roles = [];
}
return [
$this->id,
$this->password,
$this->email,
$this->locked,
$this->roles,
$this->verified,
$this->verificationLock,
];
}
/**
* @param mixed[] $data
*/
public function __unserialize(array $data): void
{
[
$this->id, // @phpstan-ignore-line
$this->password, // @phpstan-ignore-line
$this->email, // @phpstan-ignore-line
$this->locked, // @phpstan-ignore-line
$this->roles, // @phpstan-ignore-line
$this->verified, // @phpstan-ignore-line
$this->verificationLock, // @phpstan-ignore-line
] = $data;
}
public function reset(): void
{
$this->activeCreditsMembership = null;
$this->resetRoles();
}
}