Code Coverage
 
Lines
Branches
Paths
Functions and Methods
Classes and Traits
Total
93.68% covered (success)
93.68%
89 / 95
82.50% covered (warning)
82.50%
33 / 40
40.62% covered (danger)
40.62%
13 / 32
66.67% covered (warning)
66.67%
4 / 6
CRAP
0.00% covered (danger)
0.00%
0 / 1
NickGeneratorService
93.68% covered (success)
93.68%
89 / 95
82.50% covered (warning)
82.50%
33 / 40
40.62% covered (danger)
40.62%
13 / 32
66.67% covered (warning)
66.67%
4 / 6
113.31
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 computeTargetGender
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
13 / 13
41.67% covered (danger)
41.67%
5 / 12
100.00% covered (success)
100.00%
1 / 1
13.15
 buildGeneratedNick
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 updateNick
91.43% covered (success)
91.43%
32 / 35
66.67% covered (warning)
66.67%
8 / 12
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
16.67
 createNick
91.67% covered (success)
91.67%
33 / 36
70.00% covered (warning)
70.00%
7 / 10
20.00% covered (danger)
20.00%
2 / 10
0.00% covered (danger)
0.00%
0 / 1
17.80
 generateNick
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3namespace App\Service\Generator;
4
5use App\Dto\Command\GenerateNickCommand;
6use App\Dto\Command\GetWordCommand;
7use App\Dto\Result\GeneratedNickData;
8use App\Entity\Qualifier;
9use App\Entity\Subject;
10use App\Entity\Word;
11use App\Enum\GrammaticalRoleType;
12use App\Enum\OffenseLevel;
13use App\Enum\WordGender;
14use App\Exception\NickNotFoundException;
15use App\Exception\NoQualifierFoundException;
16use App\Exception\NoSubjectFoundException;
17use App\Service\Data\NickServiceInterface;
18use App\Service\Data\QualifierServiceInterface;
19use App\Service\Data\SubjectServiceInterface;
20use App\Service\Nick\NickComposerInterface;
21use App\Specification\Criteria;
22use App\Specification\Criterion\GenderConstraintType;
23use App\Specification\Criterion\GenderCriterion;
24use App\Specification\Criterion\LangCriterion;
25use App\Specification\Criterion\OffenseConstraintType;
26use App\Specification\Criterion\OffenseLevelCriterion;
27use App\Specification\Criterion\ValuesCriterion;
28use App\Specification\Criterion\ValuesCriterionCheck;
29use Random\RandomException;
30
31/**
32 * @author Wilhelm Zwertvaegher
33 */
34class NickGeneratorService implements NickGeneratorServiceInterface
35{
36    public function __construct(
37        private readonly SubjectServiceInterface $subjectService,
38        private readonly QualifierServiceInterface $qualifierService,
39        private readonly NickComposerInterface $nickComposer,
40        private readonly NickServiceInterface $nickService,
41        private readonly WordFinderInterface $wordFinder,
42    ) {
43    }
44
45    /**
46     * After a Subject has randomly been found, we have to set a target Gender for our Nick
47     * This is because the /api/word endpoint allows to replace the Subject or the Qualifier of a Nick and we have to produce
48     * consistent and compatible words
49     * For example :
50     * If the user wanted a AUTO nick, got a NEUTRAL subject,
51     * and we naively set the target gender as NEUTRAL, it may drastically reduce possibilities. This is ok for a NEUTRAL explicit request,
52     * but not an AUTO one.
53     * On the other hand we cannot simply set the target as AUTO because in that case for gender-specific words variation we
54     * would have to choose a default gender, because reloading the subject or qualifier or a nick would produce gender compatible words
55     * And we do not want to choose a gender as default, so by default we will randomly choose between M and F
56     * TL;DR ; a Nick's target Gender cannot be AUTO, it MUST be a defined GENDER.
57     *
58     * @throws RandomException
59     */
60    private function computeTargetGender(GenerateNickCommand $command, Subject $subject): WordGender
61    {
62        // in case a non-auto gender has been explicitly asked, we have to respect it
63        if (null !== $command->getGender() && WordGender::AUTO !== $command->getGender()) {
64            return $command->getGender();
65        }
66
67        // in other cases, Gender depends on the found Subject gender
68        return match ($subject->getWord()->getGender()) {
69            // neutral is randomly forced to M or F to increase possibilities, otherwise it would be very limited
70            // because in some languages neutral words are rare
71            // in any case, having a random M or F target gender will still allow NEUTRAL qualifiers
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
73            default => $subject->getWord()->getGender(),
74        };
75    }
76
77    /**
78     * Delegate the final Nick composition, and retrieve it from the system (i.e. get or create).
79     */
80    private function buildGeneratedNick(Subject $subject, Qualifier $qualifier, WordGender $targetGender): GeneratedNickData
81    {
82        $composedNick = $this->nickComposer->compose($subject, $qualifier, $subject->getWord()->getLang(), $targetGender);
83
84        $nick = $this->nickService->getOrCreate(
85            $subject,
86            $qualifier,
87            $targetGender,
88            $subject->getWord()->getOffenseLevel(),
89            $composedNick->getFinalLabel()
90        );
91
92        return new GeneratedNickData(
93            $nick->getTargetGender(),
94            $nick->getOffenseLevel(),
95            $nick,
96            $composedNick->getWords()
97        );
98    }
99
100    /**
101     * @throws NickNotFoundException
102     * @throws NoSubjectFoundException
103     * @throws NoQualifierFoundException
104     */
105    private function updateNick(GenerateNickCommand $command): GeneratedNickData
106    {
107        $previousNick = $this->nickService->getNick($command->getPreviousNickId());
108        if (null === $previousNick) {
109            throw new NickNotFoundException();
110        }
111
112        // create a new nick with a replaced word
113        $subject = $previousNick->getSubject();
114        $qualifier = $previousNick->getQualifier();
115
116        switch ($command->getReplaceRoleType()) {
117            case GrammaticalRoleType::SUBJECT:
118                /** @var Subject $subject */
119                $subject = $this->wordFinder->findSimilar(
120                    new GetWordCommand(
121                        GrammaticalRoleType::SUBJECT,
122                        // we better trust the previous nick than parameters received
123                        $previousNick->getTargetGender(),
124                        $previousNick->getOffenseLevel(),
125                        null,
126                        $subject,
127                        $command->getExclusions()
128                    )
129                );
130                if (null === $subject) {
131                    throw new NoSubjectFoundException();
132                }
133                break;
134            case GrammaticalRoleType::QUALIFIER:
135                /** @var Qualifier $qualifier */
136                $qualifier = $this->wordFinder->findSimilar(
137                    new GetWordCommand(
138                        GrammaticalRoleType::QUALIFIER,
139                        // we better trust the previous nick than parameters received
140                        $previousNick->getTargetGender(),
141                        $previousNick->getOffenseLevel(),
142                        null,
143                        $qualifier,
144                        $command->getExclusions()
145                    )
146                );
147                if (null === $qualifier) {
148                    throw new NoQualifierFoundException();
149                }
150                break;
151        }
152
153        return $this->buildGeneratedNick($subject, $qualifier, $previousNick->getTargetGender());
154    }
155
156    /**
157     * @throws NoQualifierFoundException|NoSubjectFoundException|RandomException
158     */
159    private function createNick(GenerateNickCommand $command): GeneratedNickData
160    {
161        // get a Subject according to OffenseLevel and Gender
162        $criteria = [new LangCriterion($command->getLang())];
163        $criteria[] = new GenderCriterion(
164            $command->getGender(),
165            GenderConstraintType::EXACT
166        );
167        $criteria[] = new OffenseLevelCriterion($command->getOffenseLevel(), OffenseConstraintType::EXACT);
168        if (count($command->getExclusions())) {
169            $criteria = new ValuesCriterion(Word::class, 'id', $command->getExclusions(), ValuesCriterionCheck::NOT_IN);
170        }
171
172        $subject = $this->subjectService->findOneRandomly(
173            new Criteria($criteria)
174        );
175        if (null === $subject) {
176            throw new NoSubjectFoundException();
177        }
178        $targetGender = $this->computeTargetGender($command, $subject);
179
180        $exclusions = $command->getExclusions();
181        $exclusions[] = $subject->getWord()->getId();
182
183        $criteria = [
184            new LangCriterion($command->getLang()),
185            new GenderCriterion(
186                $targetGender,
187                GenderConstraintType::RELAXED,
188            ),
189            new OffenseLevelCriterion(
190                $subject->getWord()->getOffenseLevel(),
191                OffenseLevel::MAX === $command->getOffenseLevel() ? OffenseConstraintType::EXACT : OffenseConstraintType::LTE,
192            ),
193            new ValuesCriterion(Word::class, 'id', $exclusions, ValuesCriterionCheck::NOT_IN),
194        ];
195
196        // get a Qualifier according to the Subject's OffenseLevel and Gender
197        $qualifier = $this->qualifierService->findOneRandomly(
198            new Criteria(
199                $criteria
200            )
201        );
202        if (null === $qualifier) {
203            throw new NoQualifierFoundException();
204        }
205
206        return $this->buildGeneratedNick($subject, $qualifier, $targetGender);
207    }
208
209    /**
210     * @throws NickNotFoundException|NoQualifierFoundException|NoSubjectFoundException|RandomException
211     */
212    public function generateNick(GenerateNickCommand $command): GeneratedNickData
213    {
214        // create a new Nick, or "update" an existing one
215        if ($command->getPreviousNickId()) {
216            return $this->updateNick($command);
217        }
218
219        return $this->createNick($command);
220    }
221}