Lines 96.77% 30 / 31
Functions and Methods 80.00% 4 / 5
Classes and Traits 0.00% 0 / 1
Name Lines Functions and Methods CRAP Classes and Traits
MeProcessor 96.77% 30 / 31 80.00% 4 / 5 16 0.00% 0 / 1
 __construct 100.00% 1 / 1 100.00% 1 / 1 1
 process 94.44% 17 / 18 0.00% 0 / 1 5.00
 saveParts 100.00% 6 / 6 100.00% 1 / 1 6
 saveBannergress 100.00% 3 / 3 100.00% 1 / 1 1
 sentFields 100.00% 3 / 3 100.00% 1 / 1 3
1<?php
2
3declare(strict_types=1);
4
5namespace App\State;
6
7use ApiPlatform\Metadata\Operation;
8use ApiPlatform\State\ProcessorInterface;
9use ApiPlatform\Validator\Exception\ValidationException;
10use App\ApiResource\Bannergress;
11use App\ApiResource\CustomMedals;
12use App\ApiResource\Me;
13use App\Bannergress\DoneBannerSync;
14use App\Entity\User;
15use App\Repository\UserRepository;
16use Doctrine\ORM\EntityManagerInterface;
17use Symfony\Bundle\SecurityBundle\Security;
18use Symfony\Component\HttpFoundation\Request;
19use Symfony\Component\Validator\ConstraintViolation;
20use Symfony\Component\Validator\ConstraintViolationList;
21
22/**
23 * PATCH /api/me: sets the current user's agent name (422 if another user has it), the groups
24 * of custom medals sent, the visible profile parts and the Bannergress sync; null keeps them.
25 *
26 * @implements ProcessorInterface<Me, Me>
27 */
28final readonly class MeProcessor implements ProcessorInterface
29{
30    public function __construct(
31        private EntityManagerInterface $entityManager,
32        private UserRepository $userRepository,
33        private Security $security,
34        private DoneBannerSync $doneBannerSync,
35    ) {
36    }
37
38    public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): Me
39    {
40        $user = $this->security->getUser();
41        if (!$user instanceof User) {
42            throw new \LogicException('The operation requires ROLE_USER.');
43        }
44
45        if (null !== $data->agentName) {
46            $agentName = \trim($data->agentName);
47
48            $owner = $this->userRepository->findOneByAgentName($agentName);
49            if ($owner instanceof User && $owner !== $user) {
50                throw new ValidationException(new ConstraintViolationList([new ConstraintViolation(User::AGENT_NAME_TAKEN, null, [], $data, 'agentName', $agentName)]));
51            }
52
53            $user->setAgentName($agentName);
54            $data->agentName = $agentName;
55        }
56
57        $this->saveParts($data, $user, $this->sentFields($context));
58
59        $this->entityManager->flush();
60
61        // What is stored, also for fields sent as null
62        $data->agentName = $user->getAgentName();
63        $data->customMedals = CustomMedals::fromArray($user->getCustomMedals());
64        $data->visibleParts = $user->getVisibleParts();
65        $data->visibilityChosen = $user->getVisibilityChosenAt() instanceof \DateTimeImmutable;
66        $data->bannergress = Bannergress::fromStored($user->getBannergress());
67
68        return $data;
69    }
70
71    /**
72     * The custom medals, the Bannergress sync and the visible parts, those sent. A merge patch
73     * starts from the stored user, so the sync and the parts are only saved when [$sent] has them:
74     * else a custom medals save would count as choosing the parts and bump the sync time.
75     *
76     * @param list<string> $sent
77     */
78    private function saveParts(Me $data, User $user, array $sent): void
79    {
80        if ($data->customMedals instanceof CustomMedals) {
81            $user->setCustomMedals($data->customMedals->toArray());
82        }
83
84        if ($data->bannergress instanceof Bannergress && \in_array('bannergress', $sent, true)) {
85            $this->saveBannergress($data->bannergress, $user);
86        }
87
88        if (null !== $data->visibleParts && \in_array('visibleParts', $sent, true)) {
89            $user->chooseVisibleParts($data->visibleParts, new \DateTimeImmutable());
90        }
91    }
92
93    /** The counts and the to-do banners on the user, the done list as DoneBanner. */
94    private function saveBannergress(Bannergress $bannergress, User $user): void
95    {
96        $bannergress->syncedAt = new \DateTimeImmutable()->format(\DATE_ATOM);
97        $user->setBannergress($bannergress->toArray());
98        $this->doneBannerSync->replace($user, $bannergress->doneBanners);
99    }
100
101    /**
102     * The fields of the request body.
103     *
104     * @param array<string, mixed> $context
105     *
106     * @return list<string>
107     */
108    private function sentFields(array $context): array
109    {
110        $request = $context['request'] ?? null;
111        $body = $request instanceof Request ? \json_decode($request->getContent(), true) : null;
112
113        return \is_array($body) ? \array_map(strval(...), \array_keys($body)) : [];
114    }
115}