Lines 94.54% 52 / 55
Functions and Methods 96.55% 28 / 29
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
User 94.54% 52 / 55 96.55% 28 / 29 36.21 0.00% 0 / 1
 __serialize 100.00% 4 / 4 100.00% 1 / 1 1
 __unserialize 100.00% 2 / 2 100.00% 1 / 1 1
 getRoles 100.00% 3 / 3 100.00% 1 / 1 1
 getParams 100.00% 1 / 1 100.00% 1 / 1 1
 getCustomMedals 100.00% 1 / 1 100.00% 1 / 1 1
 setCustomMedals 100.00% 2 / 2 100.00% 1 / 1 1
 getBannergress 100.00% 1 / 1 100.00% 1 / 1 1
 setBannergress 100.00% 2 / 2 100.00% 1 / 1 1
 getParam 0.00% 0 / 3 0.00% 0 / 1 6
 setParams 100.00% 2 / 2 100.00% 1 / 1 1
 getIdentifier 100.00% 1 / 1 100.00% 1 / 1 1
 getUserIdentifier 100.00% 3 / 3 100.00% 1 / 1 2
 setIdentifier 100.00% 2 / 2 100.00% 1 / 1 1
 getPassword 100.00% 1 / 1 100.00% 1 / 1 1
 getGoogleId 100.00% 1 / 1 100.00% 1 / 1 1
 setGoogleId 100.00% 2 / 2 100.00% 1 / 1 1
 getAgentName 100.00% 1 / 1 100.00% 1 / 1 1
 setAgentName 100.00% 2 / 2 100.00% 1 / 1 1
 isAgentName 100.00% 2 / 2 100.00% 1 / 1 2
 getVisibleParts 100.00% 1 / 1 100.00% 1 / 1 1
 chooseVisibleParts 100.00% 5 / 5 100.00% 1 / 1 2
 isVisible 100.00% 3 / 3 100.00% 1 / 1 3
 getAvatar 100.00% 1 / 1 100.00% 1 / 1 1
 getVisibleAvatar 100.00% 1 / 1 100.00% 1 / 1 2
 getAvatarSource 100.00% 1 / 1 100.00% 1 / 1 1
 setAvatar 100.00% 3 / 3 100.00% 1 / 1 1
 getVisibilityChosenAt 100.00% 1 / 1 100.00% 1 / 1 1
 setRole 100.00% 2 / 2 100.00% 1 / 1 1
 getRole 100.00% 1 / 1 100.00% 1 / 1 1
1<?php
2
3namespace App\Entity;
4
5use App\Enum\ProfilePart;
6use App\Enum\UserRole;
7use App\Repository\UserRepository;
8use Doctrine\DBAL\Types\Types;
9use Doctrine\ORM\Mapping\Column;
10use Doctrine\ORM\Mapping\Entity;
11use Doctrine\ORM\Mapping\GeneratedValue;
12use Doctrine\ORM\Mapping\Id;
13use Doctrine\ORM\Mapping\Table;
14use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
15use Symfony\Component\Security\Core\User\UserInterface;
16use Symfony\Component\Validator\Constraints as Assert;
17use Symfony\Component\Validator\Constraints\NotBlank;
18
19#[Entity(repositoryClass: UserRepository::class)]
20#[Table(name: 'system_user')]
21#[UniqueEntity(fields: 'identifier', message: 'This identifier is already in use')]
22#[UniqueEntity(fields: 'agentName', message: self::AGENT_NAME_TAKEN)]
23class User implements UserInterface
24{
25    /** Ingress agent names: letters, digits and underscores. */
26    public const string AGENT_NAME_PATTERN = '/^[A-Za-z0-9_]{1,50}$/';
27
28    public const string AGENT_NAME_INVALID = 'An agent name has only letters, digits and underscores.';
29
30    public const string AGENT_NAME_TAKEN = 'This agent name is already taken.';
31
32    #[Column, Id, GeneratedValue(strategy: 'SEQUENCE')]
33    public ?int $id = 0 {
34        get {
35            return $this->id;
36        }
37    }
38
39    #[Column(unique: true), NotBlank]
40    private string $identifier = '';
41
42    #[Column(type: Types::STRING, length: 50, enumType: UserRole::class)]
43    #[Assert\NotNull]
44    #[Assert\Type(type: UserRole::class, message: 'The role must be a valid user role.')]
45    private UserRole $role = UserRole::USER;
46
47    /**
48     * @var array<string>
49     */
50    #[Column(type: Types::JSON, nullable: true)]
51    private array $params = [];
52
53    /**
54     * Custom medals (anomaly, annual and event medals) as 4e-website stores them:
55     * {"anomaly": {"orion": "platinum", "echo": ""}, "annual": {"voyager": 5}, "event": {…}}.
56     *
57     * @var array<string, array<string, int|string>>
58     */
59    #[Column(type: Types::JSON, options: ['default' => '{}'])]
60    private array $customMedals = [];
61
62    /**
63     * What the app synced from Bannergress (see App\ApiResource\Bannergress::toArray()); null
64     * until it did.
65     *
66     * @var array<string, mixed>|null
67     */
68    #[Column(type: Types::JSON, nullable: true)]
69    private ?array $bannergress = null;
70
71    #[Column(length: 100, nullable: true)]
72    private ?string $googleId = null;
73
74    /**
75     * The Ingress agent name, as the agent set it. Uploaded stats must be for this agent; agent
76     * names are compared ignoring case (see UserRepository::findOneByAgentName()).
77     */
78    #[Column(length: 50, unique: true, nullable: true)]
79    #[Assert\Regex(pattern: self::AGENT_NAME_PATTERN, message: self::AGENT_NAME_INVALID)]
80    private ?string $agentName = null;
81
82    /**
83     * The profile parts other users may see (ProfilePart values); private until the user chooses.
84     *
85     * @var list<string>
86     */
87    #[Column(type: Types::JSON, options: ['default' => '[]'])]
88    private array $visibleParts = [];
89
90    /** The Google profile picture, 100 px, as a data: URI (AvatarUpdater); null without one. */
91    #[Column(type: Types::TEXT, nullable: true)]
92    private ?string $avatar = null;
93
94    /** The Google picture URL [$avatar] was made from: it is fetched again only when this changes. */
95    #[Column(length: 1000, nullable: true)]
96    private ?string $avatarSource = null;
97
98    /** When the user saved the visibility of the profile parts; null: they are asked to choose. */
99    #[Column(type: Types::DATETIME_IMMUTABLE, nullable: true)]
100    private ?\DateTimeImmutable $visibilityChosenAt = null;
101
102    /**
103     * @return array{
104     *     id: int|null,
105     *     identifier: string|null
106     * }
107     */
108    public function __serialize(): array
109    {
110        return [
111            'id' => $this->id,
112            'identifier' => $this->identifier,
113        ];
114    }
115
116    /**
117     * @param array{
118     *     id: int|null,
119     *     identifier: string|null
120     * } $data
121     */
122    public function __unserialize(array $data): void
123    {
124        $this->id = $data['id'] ?? null;
125        $this->identifier = (string) ($data['identifier'] ?? null);
126    }
127
128    #[\Override]
129    public function getRoles(): array
130    {
131        $roles = [$this->role->value];
132
133        // guarantee every user at least has ROLE_USER
134        $roles[] = UserRole::USER->value;
135
136        return \array_unique($roles);
137    }
138
139    /**
140     * @return array<string>
141     */
142    public function getParams(): array
143    {
144        return $this->params;
145    }
146
147    /**
148     * @return array<string, array<string, int|string>>
149     */
150    public function getCustomMedals(): array
151    {
152        return $this->customMedals;
153    }
154
155    /**
156     * @param array<string, array<string, int|string>> $customMedals
157     */
158    public function setCustomMedals(array $customMedals): self
159    {
160        $this->customMedals = $customMedals;
161
162        return $this;
163    }
164
165    /**
166     * @return array<string, mixed>|null
167     */
168    public function getBannergress(): ?array
169    {
170        return $this->bannergress;
171    }
172
173    /**
174     * @param array<string, mixed>|null $bannergress
175     */
176    public function setBannergress(?array $bannergress): self
177    {
178        $this->bannergress = $bannergress;
179
180        return $this;
181    }
182
183    public function getParam(string $name): string
184    {
185        return \array_key_exists($name, $this->params)
186            ? $this->params[$name]
187            : '';
188    }
189
190    /**
191     * @param array<string> $params
192     */
193    public function setParams(array $params): self
194    {
195        $this->params = $params;
196
197        return $this;
198    }
199
200    public function getIdentifier(): string
201    {
202        return $this->identifier;
203    }
204
205    #[\Override]
206    public function getUserIdentifier(): string
207    {
208        if ('' === $this->identifier) {
209            throw new \LogicException('User identifier is not set.');
210        }
211
212        return $this->identifier;
213    }
214
215    public function setIdentifier(string $identifier): self
216    {
217        $this->identifier = $identifier;
218
219        return $this;
220    }
221
222    /**
223     * @todo this method is required by the the rememberMe functionality :(
224     */
225    public function getPassword(): ?string
226    {
227        return null;
228    }
229
230    public function getGoogleId(): ?string
231    {
232        return $this->googleId;
233    }
234
235    public function setGoogleId(?string $googleId): self
236    {
237        $this->googleId = $googleId;
238
239        return $this;
240    }
241
242    public function getAgentName(): ?string
243    {
244        return $this->agentName;
245    }
246
247    public function setAgentName(?string $agentName): self
248    {
249        $this->agentName = $agentName;
250
251        return $this;
252    }
253
254    /** Whether [$nickname] (e.g. from uploaded stats) is this user's agent name, ignoring case. */
255    public function isAgentName(string $nickname): bool
256    {
257        return null !== $this->agentName
258            && \mb_strtolower(\trim($nickname)) === \mb_strtolower($this->agentName);
259    }
260
261    /**
262     * @return list<ProfilePart>
263     */
264    public function getVisibleParts(): array
265    {
266        // In the parts' order, not the order they were chosen in
267        return \array_values(\array_filter(ProfilePart::cases(), fn (ProfilePart $part): bool => \in_array($part->value, $this->visibleParts, true)));
268    }
269
270    /**
271     * Saves the user's choice. Without the agent part nothing is shown, so the others are dropped.
272     *
273     * @param list<ProfilePart> $parts
274     */
275    public function chooseVisibleParts(array $parts, \DateTimeImmutable $chosenAt): self
276    {
277        $this->visibleParts = \in_array(ProfilePart::AGENT, $parts, true)
278            ? \array_map(static fn (ProfilePart $part): string => $part->value, $parts)
279            : [];
280        $this->visibilityChosenAt = $chosenAt;
281
282        return $this;
283    }
284
285    /** Whether other users see [$part]: only with an agent name, and other parts only with the agent part. */
286    public function isVisible(ProfilePart $part): bool
287    {
288        return null !== $this->agentName
289            && \in_array(ProfilePart::AGENT->value, $this->visibleParts, true)
290            && \in_array($part->value, $this->visibleParts, true);
291    }
292
293    public function getAvatar(): ?string
294    {
295        return $this->avatar;
296    }
297
298    /** The picture other users see: only with the avatar part visible. */
299    public function getVisibleAvatar(): ?string
300    {
301        return $this->isVisible(ProfilePart::AVATAR) ? $this->avatar : null;
302    }
303
304    public function getAvatarSource(): ?string
305    {
306        return $this->avatarSource;
307    }
308
309    public function setAvatar(?string $avatar, ?string $source): self
310    {
311        $this->avatar = $avatar;
312        $this->avatarSource = $source;
313
314        return $this;
315    }
316
317    public function getVisibilityChosenAt(): ?\DateTimeImmutable
318    {
319        return $this->visibilityChosenAt;
320    }
321
322    public function setRole(UserRole $role): self
323    {
324        $this->role = $role;
325
326        return $this;
327    }
328
329    public function getRole(): UserRole
330    {
331        return $this->role;
332    }
333}