Passed
Push — master ( 7ec3d6...1eec8f )
by Johnny
02:22
created

ResponseQueue::concatContinues()   B

Complexity

Conditions 6
Paths 1

Size

Total Lines 49
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 6
eloc 27
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 49
rs 8.8657
1
<?php
2
/*
3
 * This file is part of Rivescript-php
4
 *
5
 * (c) Johnny Mast <[email protected]>
6
 *
7
 * For the full copyright and license information, please view the LICENSE
8
 * file that was distributed with this source code.
9
 */
10
11
namespace Axiom\Rivescript\Cortex\ResponseQueue;
12
13
use Axiom\Collections\Collection;
14
use Axiom\Rivescript\Cortex\Node;
15
use Axiom\Rivescript\Cortex\Trigger;
16
17
//use Axiom\Rivescript\Traits\Tags;
18
19
/**
20
 * ResponseQueue class
21
 *
22
 * The ResponseQueue releases the responses in order of sending them
23
 * back to the user.
24
 *
25
 * PHP version 7.4 and higher.
26
 *
27
 * @category Core
28
 * @package  Cortext\ResponseQueue
29
 * @author   Johnny Mast <[email protected]>
30
 * @license  https://opensource.org/licenses/MIT MIT
31
 * @link     https://github.com/axiom-labs/rivescript-php
32
 * @since    0.4.0
33
 */
34
class ResponseQueue extends Collection
35
{
36
37
//    use Tags;
38
39
    /**
40
     * A container with responses.
41
     *
42
     * @var Collection<ResponseQueueItem>
43
     */
44
    protected Collection $responses;
45
46
    /**
47
     * Store the local interpreter options
48
     * for this instance in time (since they can change).
49
     *
50
     * @var array<string, string>
51
     */
52
    protected array $options = [];
53
54
    /**
55
     * The trigger string this ResponseQueue belongs to.
56
     *
57
     * @var string
58
     */
59
    protected string $trigger = "";
60
61
    /**
62
     * ResponseQueue constructor.
63
     *
64
     * @param string $trigger the trigger this queue belongs to.
65
     */
66
    public function __construct(string $trigger = "")
67
    {
68
        parent::__construct();
69
70
        $this->responses = new Collection([]);
71
        $this->trigger = $trigger;
72
73
        $this->options = synapse()->memory->local()->all();
74
    }
75
76
    /**
77
     * Attach a response to the queue.
78
     *
79
     * @param Node  $node    The node contains information about the command.
80
     * @param array $trigger Contextual information about the trigger.
81
     *
82
     * @return void
83
     */
84
    public function attach(Node $node, Trigger $trigger): void
85
    {
86
        $type = $this->determineResponseType($node->source());
87
        $queueItem = new ResponseQueueItem($node->command(), $node->value(), $type, $trigger, $this->options);
88
        $this->responses->put($node->value(), $queueItem);
89
    }
90
91
    public function getAttachedResponses(): Collection {
92
        return $this->responses;
93
    }
94
95
    /**
96
     * Sort the responses by order.
97
     *
98
     * @param Collection<ResponseQueueItem> $responses The array containing the resources.
99
     *
100
     * @return Collection<ResponseQueueItem>
101
     */
102
    private function sortResponses(Collection $responses): Collection
103
    {
104
        return $responses->sort(
105
            function ($current, $previous) {
106
                return ($current->order < $previous->order) ? -1 : 1;
107
            }
108
        )->reverse();
109
    }
110
111
    /**
112
     * Check if a response is allowed to be returned by the bot or not.
113
     *
114
     * @param string            $response The response to validate.
115
     * @param ResponseQueueItem $item     The ResponseQueueItem.
116
     *
117
     * @return false|mixed
118
     */
119
    private function validateResponse(string $response, ResponseQueueItem $item)
120
    {
121
        $response = $this->parseTags($response);
122
        $responses = synapse()->responses;
123
124
        foreach ($responses as $class) {
125
            if (class_exists("\\Axiom\\Rivescript\\Cortex\\Responses\\{$class}")) {
126
                $class = "\\Axiom\\Rivescript\\Cortex\\Responses\\{$class}";
127
                $instance = new $class($response, $item);
128
129
                $result = $instance->parse();
130
131
                if ($result !== false) {
132
                    $item->setValue($result);
133
                    return $result;
134
                }
135
            }
136
        }
137
138
139
        return false;
140
    }
141
142
    /**
143
     * Merge the ^ continue responses to the last - response.
144
     *
145
     * @param Collection<ResponseQueueItem> $responses The array containing the responses.
146
     *
147
     * @return Collection<ResponseQueueItem>
148
     */
149
    protected function concatContinues(Collection $responses): Collection
150
    {
151
        $lastData = $responses->first();
152
        $lastResponse = "";
153
154
        $continues = Collection::make($responses->all());
155
        $continues->each(
156
            function (ResponseQueueItem $data, $response) use (&$lastData, &$lastResponse, &$continues) {
157
158
                if ($data->type === 'continue') {
159
                    $continues->remove($lastResponse);
160
                    $continues->remove($response);
161
162
                    /**
163
                     * none -- the default, nothing is added when continuation lines are joined together.
164
                     * space -- continuation lines are joined by a space character (\s)
165
                     * newline -- continuation lines are joined by a line break character (\n)
166
                     */
167
                    $options = $lastData->options;
168
                    $method = $options['concat'];
169
170
                    switch ($method) {
171
                        case 'space':
172
                            $lastResponse .= " {$response}";
173
                            break;
174
175
                        case 'newline':
176
                            $lastResponse .= "\n{$response}";
177
                            break;
178
179
                        case 'none':
180
                        default:
181
                            $lastResponse .= $response;
182
                            break;
183
                    }
184
185
                    $lastData->setValue($lastResponse);
186
                    $continues->put($lastResponse, $lastData);
187
                }
188
189
                if ($data->command !== '^') {
190
                    $lastData = $data;
191
                    $lastResponse = $response;
192
                }
193
            }
194
        );
195
196
197
        return $continues;
198
    }
199
200
    /**
201
     * Determine the order of responses by type.
202
     *
203
     * @param Collection<ResponseQueueItem> $responses The responses to inspect.
204
     *
205
     * @return Collection<ResponseQueueItem>
206
     */
207
    private function determineResponseOrder(Collection $responses): Collection
208
    {
209
        return $responses->each(
210
            function (ResponseQueueItem $data, $response) use ($responses) {
211
                if (isset($data->type)) {
212
                    switch ($data->type) {
213
                        case 'condition':
214
                            $data->order += 3000000;
215
                            break;
216
                        case 'weighted':
217
                        case 'atomic':
218
                            $data->order += 1000000;
219
                            break;
220
                    }
221
222
                    $responses->put($response, $data);
223
                }
224
            }
225
        );
226
    }
227
228
    /**
229
     * Determine the response type.
230
     *
231
     * @param string $response
232
     *
233
     * @return string
234
     */
235
    public function determineResponseType(string $response): string
236
    {
237
        $wildcards = [
238
            'weighted' => '{weight=(.+?)}',
239
            'condition' => '/^\*/',
240
            'continue' => '/^\^/',
241
            'atomic' => '/-/',
242
        ];
243
244
        foreach ($wildcards as $type => $pattern) {
245
            if (@preg_match_all($pattern, $response, $matches)) {
246
                return $type;
247
            }
248
        }
249
250
        return 'atomic';
251
    }
252
253
    /**
254
     * Parse the response through the available Tags.
255
     *
256
     * @param string $source The response string to parse.
257
     *
258
     * @return string
259
     */
260
    protected function parseTags(string $source): string
261
    {
262
263
        $source = $this->escapeUnknownTags($source);
264
265
        // This is because Tags trait does not set type response.
266
        foreach (synapse()->tags as $class) {
0 ignored issues
show
Bug Best Practice introduced by
The property tags does not exist on Axiom\Rivescript\Cortex\Synapse. Since you implemented __get, consider adding a @property annotation.
Loading history...
267
            $class = "\\Axiom\\Rivescript\\Cortex\\Tags\\{$class}";
268
            $instance = new $class("response");
269
270
            $source = $instance->parse($source, synapse()->input);
271
        }
272
273
        $source = str_replace(["&#60;", "&#62;"], ["<", ">"], $source);
274
275
        return $source;
276
        return trim($source);
0 ignored issues
show
Unused Code introduced by
return trim($source) is not reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
277
    }
278
279
    /**
280
     * Escape unknown Tags, so they don't get picked up by the parser
281
     * later on in the process.
282
     *
283
     * @param string $source The source to escape.
284
     *
285
     * @return string
286
     */
287
    public function escapeUnknownTags(string $source): string
288
    {
289
290
        $knownTags = synapse()->memory->tags()->keys()->all();
291
292
        $pattern = '/<(\S*?)*>.*?<\/\1>/s';
293
294
        preg_match_all($pattern, $source, $matches);
295
296
        $index = 0;
297
        if (is_array($matches[$index]) && isset($matches[$index][0]) && is_null($knownTags) === false && count($matches) == 2) {
298
            $matches = $matches[$index];
299
300
            foreach ($matches as $match) {
301
                $str = str_replace(['<', '>'], ["&#60;", "&#62;"], $match);
302
                $parts = explode(' ', $str);
303
                $tag = $parts[0] ?? "";
304
305
                if (in_array($tag, $knownTags, true) === false) {
0 ignored issues
show
Bug introduced by
$knownTags of type null is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

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

305
                if (in_array($tag, /** @scrutinizer ignore-type */ $knownTags, true) === false) {
Loading history...
306
                    $source = str_replace($match, $str, $source);
307
                }
308
            }
309
        }
310
311
        return $source;
312
    }
313
314
    /**
315
     * Process the Response Queue.
316
     *
317
     * @return mixed
318
     */
319
    public function process(): ?ResponseQueueItem
320
    {
321
        $sortedResponses = $this->determineResponseOrder($this->responses);
322
323
        $validResponses = new Collection([]);
324
        foreach ($sortedResponses as $response => $item) {
325
            synapse()->memory->shortTerm()->put('response', $item);
326
327
            $result = $this->validateResponse($response, $item);
328
329
            if ($result !== false) {
330
                $validResponses->put($result, $item);
331
            }
332
        }
333
334
        $validResponses = $this->concatContinues($validResponses);
335
        $validResponses = $this->sortResponses($validResponses);
336
337
        if ($validResponses->count() > 0) {
338
            return $validResponses->values()->first();
339
        }
340
341
        return null;
342
    }
343
}
344