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}

Branches

Below are the source code lines that represent each code branch as identified by Xdebug. Please note a branch is not necessarily coterminous with a line, a line may contain multiple branches and therefore show up more than once. Please also be aware that some branches may be implicit rather than explicit, e.g. an if statement always has an else as part of its logical flow even if you didn't write one.

NickGeneratorService->__construct
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    }
NickGeneratorService->buildGeneratedNick
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    }
NickGeneratorService->computeTargetGender
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()) {
63        if (null !== $command->getGender() && WordGender::AUTO !== $command->getGender()) {
63        if (null !== $command->getGender() && WordGender::AUTO !== $command->getGender()) {
64            return $command->getGender();
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,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
72            WordGender::AUTO, WordGender::NEUTRAL => 1 === random_int(0, 1) ? WordGender::M : WordGender::F,
73            default => $subject->getWord()->getGender(),
73            default => $subject->getWord()->getGender(),
74        };
75    }
NickGeneratorService->createNick
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(
172        $subject = $this->subjectService->findOneRandomly(
173            new Criteria($criteria)
174        );
175        if (null === $subject) {
176            throw new NoSubjectFoundException();
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,
191                OffenseLevel::MAX === $command->getOffenseLevel() ? OffenseConstraintType::EXACT : OffenseConstraintType::LTE,
191                OffenseLevel::MAX === $command->getOffenseLevel() ? OffenseConstraintType::EXACT : OffenseConstraintType::LTE,
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();
206        return $this->buildGeneratedNick($subject, $qualifier, $targetGender);
207    }
NickGeneratorService->generateNick
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);
219        return $this->createNick($command);
220    }
NickGeneratorService->updateNick
105    private function updateNick(GenerateNickCommand $command): GeneratedNickData
106    {
107        $previousNick = $this->nickService->getNick($command->getPreviousNickId());
108        if (null === $previousNick) {
109            throw new NickNotFoundException();
113        $subject = $previousNick->getSubject();
114        $qualifier = $previousNick->getQualifier();
115
116        switch ($command->getReplaceRoleType()) {
117            case GrammaticalRoleType::SUBJECT:
134            case GrammaticalRoleType::QUALIFIER:
134            case GrammaticalRoleType::QUALIFIER:
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();
133                break;
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();
150                break;
150                break;
151        }
152
153        return $this->buildGeneratedNick($subject, $qualifier, $previousNick->getTargetGender());
154    }
{main}
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    }