src/Entity/User.php line 45

Open in your IDE?
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Entity;
  4. use App\Contracts\User\Status;
  5. use App\Contracts\User\UserInterface;
  6. use DateTime;
  7. use DateTimeImmutable;
  8. use Doctrine\Common\Collections\ArrayCollection;
  9. use Doctrine\Common\Collections\Collection;
  10. use Doctrine\ORM\Mapping\Column;
  11. use Doctrine\ORM\Mapping\DiscriminatorColumn;
  12. use Doctrine\ORM\Mapping\DiscriminatorMap;
  13. use Doctrine\ORM\Mapping\Entity;
  14. use Doctrine\ORM\Mapping\GeneratedValue;
  15. use Doctrine\ORM\Mapping\HasLifecycleCallbacks;
  16. use Doctrine\ORM\Mapping\Id;
  17. use Doctrine\ORM\Mapping\Index;
  18. use Doctrine\ORM\Mapping\InheritanceType;
  19. use Doctrine\ORM\Mapping\OneToMany;
  20. use Doctrine\ORM\Mapping\OneToOne;
  21. use Doctrine\ORM\Mapping\PrePersist;
  22. use Doctrine\ORM\Mapping\PreUpdate;
  23. use Doctrine\ORM\Mapping\UniqueConstraint;
  24. use LogicException;
  25. use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
  26. use Symfony\Component\Security\Core\User\EquatableInterface;
  27. use Symfony\Component\Security\Core\User\UserInterface as SymfonyUserInterface;
  28. #[Entity]
  29. #[Index(columns: ['language'], name'idx_user_language')]
  30. #[UniqueConstraint('unique_email', ['email'])]
  31. #[UniqueEntity('email''Email is already used')]
  32. #[HasLifecycleCallbacks]
  33. #[InheritanceType('JOINED')]
  34. #[DiscriminatorColumn(name'role'type'string')]
  35. #[DiscriminatorMap([
  36.     Administrator::ROLE_NAME => Administrator::class,
  37.     Creator::ROLE_NAME => Creator::class,
  38.     Sponsor::ROLE_NAME => Sponsor::class,
  39.     Manager::ROLE_NAME => Manager::class,
  40. ])]
  41. class User implements UserInterfaceEquatableInterface
  42. {
  43.     public const ROLE_NAME null;
  44.     public const SOFT_DELETE_DAYS 30;
  45.     /** @var string */
  46.     public const BIRTHDAY_FORMAT 'm/d/Y';
  47.     /** @var string */
  48.     private const AVATAR_FILE_NAME_TEMPLATE 'user.%d.avatar';
  49.     #[Id]
  50.     #[GeneratedValue]
  51.     #[Column(type'bigint'options: ['unsigned' => true])]
  52.     private ?int $id null;
  53.     #[Column(name'status'type'smallint')]
  54.     private int $status Status::CREATED;
  55.     #[Column(name'email'type'string'nullabletrue)]
  56.     private ?string $email null;
  57.     #[Column(name'phone_number'type'bigint'nullabletrueoptions: ['unsigned' => true])]
  58.     private ?int $phoneNumber null;
  59.     #[Column(name'first_name'type'string'nullabletrue)]
  60.     private ?string $firstName null;
  61.     #[Column(name'last_name'type'string'nullabletrue)]
  62.     private ?string $lastName null;
  63.     /**
  64.      * Primary posting language as an ISO-639-1 code (e.g. 'en', 'es', 'pt'), derived from the mode of
  65.      * the creator's recent post textLanguage (profile language fallback). Stored on the profile's user
  66.      * row (creator.id === user.id). Per-profile only — not shared across owning-account peers.
  67.      */
  68.     #[Column(type'string'length8nullabletrue)]
  69.     private ?string $language null;
  70.     #[Column(type'datetime'nullabletrue)]
  71.     private ?DateTime $blockedUntil;
  72.     #[Column(type'datetime'nullablefalse)]
  73.     private DateTime $createdAt;
  74.     #[Column(type'datetime'nullabletrue)]
  75.     private ?DateTime $updatedAt null;
  76.     #[Column('deleted_at''datetime'nullabletrue)]
  77.     private ?DateTime $deletedAt null;
  78.     #[OneToOne(mappedBy'user'targetEntityUserLocationPermission::class)]
  79.     private ?UserLocationPermission $locationPermissionTelemetry null;
  80.     #[OneToOne(mappedBy'user'targetEntityPaymentBalance::class)]
  81.     private ?PaymentBalance $paymentBalance null;
  82.     #[OneToOne(mappedBy'user'targetEntityReferral::class)]
  83.     private ?Referral $referral null;
  84.     #[OneToMany(mappedBy'referredBy'targetEntityReferral::class)]
  85.     private Collection $referrals;
  86.     #[PrePersist]
  87.     public function prePersist(): void
  88.     {
  89.         $this->createdAt = new DateTime();
  90.     }
  91.     #[PreUpdate]
  92.     public function preUpdate(): void
  93.     {
  94.         $this->updatedAt = new DateTime();
  95.     }
  96.     public function getId(): ?int
  97.     {
  98.         return $this->id;
  99.     }
  100.     /**
  101.      * Security tokens serialize the user into the session on EVERY request. Serializing the
  102.      * full entity drags along any initialized Doctrine relations (Manager::campaigns ->
  103.      * activities -> creators ...): observed 16MB+ session payloads (Jun 11 2026) that
  104.      * overflowed session storage and silently signed managers out — see
  105.      * docs/signouts-approval-dropped-timeout.md. Persist an identity shell only; the
  106.      * security ContextListener reloads the full entity from the user provider on every
  107.      * request, and isEqualTo() keeps the refreshed token valid.
  108.      */
  109.     public function __serialize(): array
  110.     {
  111.         return [
  112.             'id' => $this->id,
  113.             'email' => $this->email,
  114.             'status' => $this->status,
  115.         ];
  116.     }
  117.     public function __unserialize(array $data): void
  118.     {
  119.         // Accept both the identity-shell format and the legacy full-entity format (sessions
  120.         // written before this change carry PHP's mangled "\0Class\0prop" keys for every
  121.         // property) so already-authenticated users survive the deploy without a logout.
  122.         $legacy "\0" self::class . "\0";
  123.         $this->id $data['id'] ?? $data[$legacy 'id'] ?? null;
  124.         $this->email $data['email'] ?? $data[$legacy 'email'] ?? null;
  125.         $this->status $data['status'] ?? $data[$legacy 'status'] ?? Status::CREATED;
  126.     }
  127.     /**
  128.      * Used by the security layer after refreshing the user from the provider. Identity (same
  129.      * concrete class + id) is sufficient — and required: the serialized shell intentionally
  130.      * carries no password (subclass typed $password properties are uninitialized on it), so
  131.      * Symfony's default password/salt comparison must never run.
  132.      */
  133.     public function isEqualTo(SymfonyUserInterface $user): bool
  134.     {
  135.         return $user instanceof static
  136.             && $this->getId() !== null
  137.             && $user->getId() === $this->getId();
  138.     }
  139.     public function getUsername(): string
  140.     {
  141.         return $this->email ?? (string)$this->id;
  142.     }
  143.     public function getFullname(): string
  144.     {
  145.         $fullName trim(sprintf('%s %s'$this->firstName$this->lastName));
  146.         return $fullName === '' $this->getUsername() : $fullName;
  147.     }
  148.     public function getPassword(): string
  149.     {
  150.         return '';
  151.     }
  152.     public function getSalt()
  153.     {
  154.         return null;
  155.     }
  156.     public function getUserIdentifier(): string
  157.     {
  158.         return (string)$this->getId();
  159.     }
  160.     public function eraseCredentials()
  161.     {
  162.         // TODO: Implement eraseCredentials() method.
  163.     }
  164.     public function getRoles(): array
  165.     {
  166.         if (static::ROLE_NAME === null) {
  167.             throw new LogicException('Undefined role name.');
  168.         }
  169.         return [static::ROLE_NAME];
  170.     }
  171.     public function isFinishedRegistration(): bool
  172.     {
  173.         return $this->checkStatus(...Status::FINISHED_LIST);
  174.     }
  175.     public function isFirstOpen(): bool
  176.     {
  177.         return !$this->checkStatus(Status::FIRST_OPEN);
  178.     }
  179.     public function firstOpen(): void
  180.     {
  181.         $this->status |= Status::FIRST_OPEN;
  182.     }
  183.     public function isEmailVerified(): bool
  184.     {
  185.         return $this->checkStatus(Status::EMAIL_VERIFIED);
  186.     }
  187.     public function isPhoneVerified(): bool
  188.     {
  189.         return $this->checkStatus(Status::PHONE_VERIFIED);
  190.     }
  191.     public function verifyPhone(): static
  192.     {
  193.         $this->status |= Status::PHONE_VERIFIED;
  194.         return $this;
  195.     }
  196.     public function verifyEmail(): static
  197.     {
  198.         $this->status |= Status::EMAIL_VERIFIED;
  199.         return $this;
  200.     }
  201.     private function checkStatus(int ...$statusList): bool
  202.     {
  203.         foreach ($statusList as $status) {
  204.             if (($this->status $status) === 0) {
  205.                 return false;
  206.             }
  207.         }
  208.         return true;
  209.     }
  210.     public function setFirstName(string $firstName): static
  211.     {
  212.         $this->firstName $firstName;
  213.         return $this;
  214.     }
  215.     public function setLastName(string $lastName): static
  216.     {
  217.         $this->lastName $lastName;
  218.         return $this;
  219.     }
  220.     public function name(string $firstNamestring $lastName): self
  221.     {
  222.         $this->setFirstName($firstName);
  223.         $this->setLastName($lastName);
  224.         return $this;
  225.     }
  226.     public function baseInfo(string $firstNamestring $lastName): self
  227.     {
  228.         $this->firstName $firstName;
  229.         $this->lastName $lastName;
  230.         return $this;
  231.     }
  232.     public function getFirstName(): ?string
  233.     {
  234.         return $this->firstName;
  235.     }
  236.     public function getLastName(): ?string
  237.     {
  238.         return $this->lastName;
  239.     }
  240.     /**
  241.      * @internal Doctrine-mapped field; use {@see Creator::getLanguage()} / {@see Creator::setLanguage()}.
  242.      */
  243.     protected function getLanguageValue(): ?string
  244.     {
  245.         return $this->language;
  246.     }
  247.     /**
  248.      * @internal Doctrine-mapped field; use {@see Creator::setLanguage()}.
  249.      */
  250.     protected function setLanguageValue(?string $language): static
  251.     {
  252.         $this->language $language;
  253.         return $this;
  254.     }
  255.     public function setPhoneNumber(?string $phoneNumber): self
  256.     {
  257.         if ($phoneNumber !== null) {
  258.             $phoneNumber = (int)preg_replace('/[^\d]/'''$phoneNumber);
  259.             if ($this->isPhoneVerified() && $this->phoneNumber !== $phoneNumber) {
  260.                 $this->status ^= Status::PHONE_VERIFIED;
  261.             }
  262.         }
  263.         $this->phoneNumber $phoneNumber;
  264.         return $this;
  265.     }
  266.     public function getPhoneNumber(): ?string
  267.     {
  268.         if ($this->phoneNumber === null) {
  269.             return null;
  270.         }
  271.         return sprintf('+%d'preg_replace('/[^\d]/''', (string)($this->phoneNumber ?? '')));
  272.     }
  273.     public function setEmail(?string $email): self
  274.     {
  275.         if ($email !== null && $this->isEmailVerified() && $this->email !== $email) {
  276.             $this->status ^= Status::EMAIL_VERIFIED;
  277.         }
  278.         $this->email $email;
  279.         return $this;
  280.     }
  281.     public function getEmail(): ?string
  282.     {
  283.         return $this->email;
  284.     }
  285.     public function getAvatarFileName(): string
  286.     {
  287.         return sprintf(self::AVATAR_FILE_NAME_TEMPLATE$this->getId());
  288.     }
  289.     public function getDeletedAt(): ?DateTime
  290.     {
  291.         return $this->deletedAt;
  292.     }
  293.     public function getBlockedUntil(): ?DateTime
  294.     {
  295.         return $this->blockedUntil;
  296.     }
  297.     public function setDeletedAt(bool $isDeleted true): self
  298.     {
  299.         $this->deletedAt $isDeleted ? new DateTime() : null;
  300.         return $this;
  301.     }
  302.     public function isDeleted(): bool
  303.     {
  304.         return $this->getDeletedAt() instanceof DateTime;
  305.     }
  306.     public function isSoftDeleted(): bool
  307.     {
  308.         if (!$this->isDeleted()) {
  309.             return false;
  310.         }
  311.         $now = new DateTimeImmutable();
  312.         $interval $now->diff($this->getDeletedAt());
  313.         return $interval->days self::SOFT_DELETE_DAYS;
  314.     }
  315.     public function block(?DateTime $blockedUntil null): static
  316.     {
  317.         if (!$this->checkStatus(Status::BLOCKED)) {
  318.             $this->status |= Status::BLOCKED;
  319.             $this->blockedUntil $blockedUntil;
  320.         }
  321.         return $this;
  322.     }
  323.     public function unblock(): static
  324.     {
  325.         $this->status ^= Status::BLOCKED;
  326.         $this->blockedUntil null;
  327.         return $this;
  328.     }
  329.     public function isBlocked(): bool
  330.     {
  331.         return $this->checkStatus(Status::BLOCKED)
  332.             && ($this->blockedUntil === null || $this->blockedUntil > new DateTime());
  333.     }
  334.     public function getCreatedAt(): DateTime
  335.     {
  336.         return $this->createdAt;
  337.     }
  338.     public function getUpdatedAt(): ?DateTime
  339.     {
  340.         return $this->updatedAt;
  341.     }
  342.     public function isRole(string $role): bool
  343.     {
  344.         return in_array($role$this->getRoles(), true);
  345.     }
  346.     public function setPaymentBalance(PaymentBalance $paymentBalance): static
  347.     {
  348.         $this->paymentBalance $paymentBalance;
  349.         return $this;
  350.     }
  351.     public function getPaymentBalance(): ?PaymentBalance
  352.     {
  353.         return $this->paymentBalance;
  354.     }
  355.     public function getReferral(): ?Referral
  356.     {
  357.         return $this->referral;
  358.     }
  359.     public function getReferrals(): Collection
  360.     {
  361.         return $this->referrals;
  362.     }
  363.     public function getLocationPermissionTelemetry(): ?UserLocationPermission
  364.     {
  365.         return $this->locationPermissionTelemetry;
  366.     }
  367.     public function setLocationPermissionTelemetry(?UserLocationPermission $locationPermissionTelemetry): static
  368.     {
  369.         $this->locationPermissionTelemetry $locationPermissionTelemetry;
  370.         return $this;
  371.     }
  372. }