ConversationManager   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 234
Duplicated Lines 0 %

Test Coverage

Coverage 66.18%

Importance

Changes 0
Metric Value
wmc 31
eloc 61
dl 0
loc 234
ccs 45
cts 68
cp 0.6618
rs 9.92
c 0
b 0
f 0

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 4 1
A registerFallbackIntent() 0 3 1
A __destruct() 0 16 5
B matchIntent() 0 23 8
A markAsTransitioned() 0 3 1
A transitioned() 0 3 1
A resolveContext() 0 24 3
A setReceivedMessage() 0 3 1
A saveContext() 0 5 1
A registerIntent() 0 3 1
A getContext() 0 7 2
A getCacheKeyForContext() 0 3 1
A restartInteraction() 0 7 1
A flushContext() 0 4 1
A converse() 0 9 2
A getIntents() 0 3 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace FondBot\Conversation;
6
7
use Closure;
8
use FondBot\Channels\Chat;
9
use FondBot\Channels\User;
10
use FondBot\Channels\Channel;
11
use FondBot\Contracts\Activator;
12
use Illuminate\Cache\Repository;
13
use FondBot\Contracts\Conversable;
14
use FondBot\Events\MessageReceived;
15
use Illuminate\Contracts\Foundation\Application;
16
17
class ConversationManager
18
{
19
    private $intents = [];
20
    private $fallbackIntent;
21
22
    private $application;
23
    private $cache;
24
25
    private $transitioned = false;
26
27
    private $messageReceived;
28 82
29
    public function __construct(Application $application, Repository $cache)
30 82
    {
31 82
        $this->application = $application;
32 82
        $this->cache = $cache;
33
    }
34
35 2
    /**
36
     * Register intent.
37 2
     *
38 2
     * @param string $class
39
     */
40
    public function registerIntent(string $class): void
41 82
    {
42
        $this->intents[] = $class;
43 82
    }
44 82
45
    /**
46
     * Register fallback intent.
47 1
     *
48
     * @param string $class
49 1
     */
50
    public function registerFallbackIntent(string $class): void
51
    {
52
        $this->fallbackIntent = $class;
53 1
    }
54
55 1
    /**
56
     * Get all registered intents.
57 1
     *
58
     * @return array
59 1
     */
60 1
    public function getIntents(): array
61 1
    {
62
        return $this->intents;
63
    }
64
65
    /**
66
     * Match intent by received message.
67 1
     *
68
     * @param MessageReceived $messageReceived
69
     *
70
     * @return Intent|null
71 1
     */
72
    public function matchIntent(MessageReceived $messageReceived): ?Intent
73 1
    {
74 1
        foreach ($this->intents as $intent) {
75 1
            /** @var Intent $intent */
76
            $intent = resolve($intent);
0 ignored issues
show
Bug introduced by
$intent of type FondBot\Conversation\Intent is incompatible with the type string expected by parameter $name of resolve(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

76
            $intent = resolve(/** @scrutinizer ignore-type */ $intent);
Loading history...
77
78
            foreach ($intent->activators() as $activator) {
79
                if (!$intent->passesAuthorization($messageReceived)) {
80
                    continue;
81 1
                }
82
83 1
                if ($activator instanceof Closure && value($activator($messageReceived)) === true) {
84 1
                    return $intent;
85
                }
86
87 1
                if ($activator instanceof Activator && $activator->matches($messageReceived)) {
88 1
                    return $intent;
89
                }
90
            }
91
        }
92 1
93
        // Otherwise, return fallback intent
94 1
        return resolve($this->fallbackIntent);
0 ignored issues
show
Bug Best Practice introduced by
The expression return resolve($this->fallbackIntent) could return the type Illuminate\Foundation\Application which is incompatible with the type-hinted return FondBot\Conversation\Intent|null. Consider adding an additional type-check to rule them out.
Loading history...
95
    }
96
97
    /**
98 1
     * Resolve conversation context.
99
     *
100 1
     * @param Channel $channel
101 1
     * @param Chat    $chat
102 1
     * @param User    $user
103
     *
104 1
     * @return Context
105
     */
106
    public function resolveContext(Channel $channel, Chat $chat, User $user): Context
107
    {
108
        $value = $this->cache->get($this->getCacheKeyForContext($channel, $chat, $user), [
109
            'chat' => $chat,
110
            'user' => $user,
111
            'intent' => null,
112
            'interaction' => null,
113
            'items' => [],
114
        ]);
115 82
116
        $context = new Context($channel, $chat, $user, $value['items'] ?? []);
117 82
118 82
        if (isset($value['intent'])) {
119
            $context->setIntent(resolve($value['intent']));
0 ignored issues
show
Bug introduced by
It seems like resolve($value['intent']) can also be of type Illuminate\Foundation\Application; however, parameter $intent of FondBot\Conversation\Context::setIntent() does only seem to accept FondBot\Conversation\Intent, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

119
            $context->setIntent(/** @scrutinizer ignore-type */ resolve($value['intent']));
Loading history...
120
        }
121 15
122
        if (isset($value['interaction'])) {
123
            $context->setInteraction(resolve($value['interaction']));
0 ignored issues
show
Bug introduced by
It seems like resolve($value['interaction']) can also be of type Illuminate\Foundation\Application; however, parameter $interaction of FondBot\Conversation\Context::setInteraction() does only seem to accept null|FondBot\Conversation\Interaction, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

123
            $context->setInteraction(/** @scrutinizer ignore-type */ resolve($value['interaction']));
Loading history...
124
        }
125
126
        // Bind resolved instance to the container
127
        $this->application->instance('fondbot.conversation.context', $context);
128
129
        return $context;
130
    }
131
132
    /**
133
     * Save context.
134
     *
135
     * @param Context $context
136
     */
137
    public function saveContext(Context $context): void
138
    {
139
        $this->cache->forever(
140
            $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser()),
141
            $context->toArray()
142
        );
143
    }
144
145
    /**
146
     * Flush context.
147
     *
148
     * @param Context $context
149
     */
150
    public function flushContext(Context $context): void
151
    {
152
        $this->cache->forget(
153
            $this->getCacheKeyForContext($context->getChannel(), $context->getChat(), $context->getUser())
154 82
        );
155
    }
156 82
157
    /**
158 82
     * Get current context.
159 82
     *
160
     * @return Context|null
161
     */
162
    public function getContext(): ?Context
163
    {
164
        if (!$this->application->has('fondbot.conversation.context')) {
165
            return null;
166
        }
167
168
        return $this->application->get('fondbot.conversation.context');
169
    }
170
171
    /**
172
     * Define received message.
173 2
     *
174
     * @param MessageReceived $messageReceived
175 2
     */
176
    public function setReceivedMessage(MessageReceived $messageReceived): void
177
    {
178
        $this->messageReceived = $messageReceived;
179
    }
180
181
    /**
182
     * Mark conversation as transitioned.
183
     */
184
    public function markAsTransitioned(): void
185
    {
186
        $this->transitioned = true;
187
    }
188
189
    /**
190
     * Determine if conversation has been transitioned.
191
     *
192
     * @return bool
193
     */
194
    public function transitioned(): bool
195
    {
196
        return $this->transitioned;
197
    }
198
199
    /**
200
     * Start conversation.
201
     *
202
     * @param Conversable     $conversable
203
     */
204
    public function converse(Conversable $conversable): void
205
    {
206
        context()->incrementAttempts();
207
208
        if ($conversable instanceof Intent) {
209
            context()->setIntent($conversable)->setInteraction(null);
210
        }
211
212
        $conversable->handle($this->messageReceived);
213
    }
214
215
    /**
216
     * Restart interaction.
217
     *
218
     * @param Interaction $interaction
219
     */
220
    public function restartInteraction(Interaction $interaction): void
221
    {
222
        context()->setInteraction(null);
223
224
        $this->converse($interaction);
225
226
        $this->markAsTransitioned();
227
    }
228
229
    public function __destruct()
230
    {
231
        $context = $this->getContext();
232
233
        if ($context === null) {
234
            return;
235
        }
236
237
        // Close session if conversation has not been transitioned
238
        if (!$this->transitioned()) {
239
            $this->flushContext($context);
240
        }
241
242
        // Save context if exists
243
        if ($this->transitioned() && $context = context()) {
244
            $this->saveContext($context);
245
        }
246
    }
247
248
    private function getCacheKeyForContext(Channel $channel, Chat $chat, User $user): string
249
    {
250
        return implode('.', ['context', $channel->getName(), $chat->getId(), $user->getId()]);
251
    }
252
}
253