Code Coverage |
||||||||||||||||
Lines |
Branches |
Paths |
Functions and Methods |
Classes and Traits |
||||||||||||
| Total | |
93.68% |
89 / 95 |
|
82.50% |
33 / 40 |
|
40.62% |
13 / 32 |
|
66.67% |
4 / 6 |
CRAP | |
0.00% |
0 / 1 |
| NickGeneratorService | |
93.68% |
89 / 95 |
|
82.50% |
33 / 40 |
|
40.62% |
13 / 32 |
|
66.67% |
4 / 6 |
113.31 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| computeTargetGender | |
100.00% |
6 / 6 |
|
100.00% |
13 / 13 |
|
41.67% |
5 / 12 |
|
100.00% |
1 / 1 |
13.15 | |||
| buildGeneratedNick | |
100.00% |
14 / 14 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| updateNick | |
91.43% |
32 / 35 |
|
66.67% |
8 / 12 |
|
33.33% |
2 / 6 |
|
0.00% |
0 / 1 |
16.67 | |||
| createNick | |
91.67% |
33 / 36 |
|
70.00% |
7 / 10 |
|
20.00% |
2 / 10 |
|
0.00% |
0 / 1 |
17.80 | |||
| generateNick | |
100.00% |
3 / 3 |
|
100.00% |
3 / 3 |
|
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 | |||
| 1 | <?php |
| 2 | |
| 3 | namespace App\Service\Generator; |
| 4 | |
| 5 | use App\Dto\Command\GenerateNickCommand; |
| 6 | use App\Dto\Command\GetWordCommand; |
| 7 | use App\Dto\Result\GeneratedNickData; |
| 8 | use App\Entity\Qualifier; |
| 9 | use App\Entity\Subject; |
| 10 | use App\Entity\Word; |
| 11 | use App\Enum\GrammaticalRoleType; |
| 12 | use App\Enum\OffenseLevel; |
| 13 | use App\Enum\WordGender; |
| 14 | use App\Exception\NickNotFoundException; |
| 15 | use App\Exception\NoQualifierFoundException; |
| 16 | use App\Exception\NoSubjectFoundException; |
| 17 | use App\Service\Data\NickServiceInterface; |
| 18 | use App\Service\Data\QualifierServiceInterface; |
| 19 | use App\Service\Data\SubjectServiceInterface; |
| 20 | use App\Service\Nick\NickComposerInterface; |
| 21 | use App\Specification\Criteria; |
| 22 | use App\Specification\Criterion\GenderConstraintType; |
| 23 | use App\Specification\Criterion\GenderCriterion; |
| 24 | use App\Specification\Criterion\LangCriterion; |
| 25 | use App\Specification\Criterion\OffenseConstraintType; |
| 26 | use App\Specification\Criterion\OffenseLevelCriterion; |
| 27 | use App\Specification\Criterion\ValuesCriterion; |
| 28 | use App\Specification\Criterion\ValuesCriterionCheck; |
| 29 | use Random\RandomException; |
| 30 | |
| 31 | /** |
| 32 | * @author Wilhelm Zwertvaegher |
| 33 | */ |
| 34 | class 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 | } |