Passed
Push — master ( e730bc...7f6736 )
by Charlotte
02:08
created

Argument::__construct()   B

Complexity

Conditions 10
Paths 18

Size

Total Lines 36
Code Lines 29

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 10
eloc 29
nc 18
nop 2
dl 0
loc 36
rs 7.6666
c 0
b 0
f 0

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Livia
4
 * Copyright 2017-2018 Charlotte Dunois, All Rights Reserved
5
 *
6
 * Website: https://charuru.moe
7
 * License: https://github.com/CharlotteDunois/Livia/blob/master/LICENSE
8
*/
9
10
namespace CharlotteDunois\Livia\Arguments;
11
12
/**
13
 * A fancy argument.
14
 *
15
 * @property \CharlotteDunois\Livia\LiviaClient               $client        The client which initiated the instance.
16
 * @property string                                           $key           Key for the argument.
17
 * @property string                                           $label         Label for the argument.
18
 * @property string                                           $prompt        Question prompt for the argument.
19
 * @property string|null                                      $typeID        Type name of the argument.
20
 * @property int|float|null                                   $max           If type is integer or float, this is the maximum value of the number. If type is string, this is the maximum length of the string.
21
 * @property int|float|null                                   $min           If type is integer or float, this is the minimum value of the number. If type is string, this is the minimum length of the string.
22
 * @property mixed|null                                       $default       The default value for the argument.
23
 * @property bool                                             $infinite      Whether the argument accepts an infinite number of values.
24
 * @property callable|null                                    $validate      Validator function for validating a value for the argument. ({@see \CharlotteDunois\Livia\Types\ArgumentType::validate})
25
 * @property callable|null                                    $parse         Parser function to parse a value for the argument. ({@see \CharlotteDunois\Livia\Types\ArgumentType::parse})
26
 * @property callable|null                                    $emptyChecker  Empty checker function for the argument. ({@see \CharlotteDunois\Livia\Types\ArgumentType::isEmpty})
27
 * @property int                                              $wait          How long to wait for input (in seconds).
28
 *
29
 * @property \CharlotteDunois\Livia\Types\ArgumentType|null   $type          Type of the argument.
30
 */
31
class Argument implements \Serializable {
32
    /**
33
     * The client which initiated the instance.
34
     * @var \CharlotteDunois\Livia\LiviaClient
35
     */
36
    protected $client;
37
    
38
    /**
39
     * Key for the argument.
40
     * @var string
41
     */
42
    protected $key;
43
    
44
    /**
45
     * Label for the argument.
46
     * @var string
47
     */
48
    protected $label;
49
    
50
    /**
51
     * Question prompt for the argument.
52
     * @var string
53
     */
54
    protected $prompt;
55
    
56
    /**
57
     * If type is integer or float, this is the maximum value of the number. If type is string, this is the maximum length of the string.
58
     * @var int|float|null
59
     */
60
    protected $max;
61
    
62
    /**
63
     * If type is integer or float, this is the minimum value of the number. If type is string, this is the minimum length of the string.
64
     * @var int|float|null
65
     */
66
    protected $min;
67
    
68
    /**
69
     * The default value for the argument.
70
     * @var mixed|null
71
     */
72
    protected $default;
73
    
74
    /**
75
     * Whether the argument accepts an infinite number of values.
76
     * @var bool
77
     */
78
    protected $infinite;
79
    
80
    /**
81
     * Validator function for validating a value for the argument.
82
     * @var callable|null
83
     */
84
    protected $validate;
85
    
86
    /**
87
     * Parser function to parse a value for the argument.
88
     * @var callable|null
89
     */
90
    protected $parse;
91
    
92
    /**
93
     * Empty checker function for the argument.
94
     * @var callable|null
95
     */
96
    protected $emptyChecker;
97
    
98
    /**
99
     * How long to wait for input (in seconds).
100
     * @var int
101
     */
102
    protected $wait;
103
    
104
    /**
105
     * Type name of the argument.
106
     * @var string|null
107
     */
108
    protected $typeID;
109
    
110
    /**
111
     * Constructs a new Argument. Info is an array as following:
112
     *
113
     * ```
114
     * array(
115
     *   'key' => string, (Key for the argument)
116
     *   'label' => string, (Label for the argument, defaults to key)
117
     *   'prompt' => string, (First prompt for the argument when it wasn't specified)
118
     *   'type' => string|null, (Type of the argument, must be the ID of one of the registered argument types)
119
     *   'max' => int|float, (If type is integer or float this is the maximum value, if type is string this is the maximum length, optional)
120
     *   'min' => int|float, (If type is integer or float this is the minimum value, if type is string this is the minimum length, optional)
121
     *   'default' => mixed, (Default value for the argumen, must not be null, optional)
122
     *   'infinite' => bool, (Infinite argument collecting, defaults to false)
123
     *   'validate' => callable, (Validator function for the argument, optional)
124
     *   'parse' => callable, (Parser function for the argument, optional)
125
     *   'emptyChecker' => callable, (Empty checker function for the argument, optional)
126
     *   'wait' => int (how long to wait for input, in seconds)
127
     * )
128
     * ```
129
     *
130
     * @param \CharlotteDunois\Livia\LiviaClient    $client
131
     * @param array                                 $info
132
     * @throws \InvalidArgumentException
133
     */
134
    function __construct(\CharlotteDunois\Livia\LiviaClient $client, array $info) {
135
        $this->client = $client;
136
        
137
        \CharlotteDunois\Validation\Validator::make($info, array(
138
            'key' => 'required|string|min:1',
139
            'prompt' => 'required|string|min:1',
140
            'type' => 'string|min:1|nullable',
141
            'max' => 'integer|float',
142
            'min' => 'integer|float',
143
            'infinite' => 'boolean',
144
            'validate' => 'callable',
145
            'parse' => 'callable',
146
            'emptyChecker' => 'callable',
147
            'wait' => 'integer|min:1'
148
        ))->throw(\InvalidArgumentException::class);
149
        
150
        if(empty($info['type']) && (empty($info['validate']) || empty($info['parse']))) {
151
            throw new \InvalidArgumentException('Argument type can\'t be empty if you don\'t implement validate and parse function');
152
        }
153
        
154
        if(!empty($info['type']) && !$this->client->registry->types->has($info['type'])) {
155
            throw new \InvalidArgumentException('Argument type "'.$info['type'].'" is not registered');
156
        }
157
        
158
        $this->key = $info['key'];
159
        $this->label = (!empty($info['label']) ? $info['label'] : $info['key']);
160
        $this->prompt = $info['prompt'];
161
        $this->typeID = $info['type'] ?? null;
162
        $this->max = $info['max'] ?? null;
163
        $this->min = $info['min'] ?? null;
164
        $this->default = $info['default'] ?? null;
165
        $this->infinite = (!empty($info['infinite']));
166
        $this->validate = (!empty($info['validate']) ? $info['validate'] : null);
167
        $this->parse = (!empty($info['parse']) ? $info['parse'] : null);;
168
        $this->emptyChecker = (!empty($info['emptyChecker']) ? $info['emptyChecker'] : null);
169
        $this->wait = $info['wait'] ?? 30;
170
    }
171
    
172
    /**
173
     * @param string  $name
174
     * @return bool
175
     * @throws \Exception
176
     * @internal
177
     */
178
    function __isset($name) {
179
        try {
180
            return $this->$name !== null;
181
        } catch (\RuntimeException $e) {
182
            if($e->getTrace()[0]['function'] === '__get') {
183
                return false;
184
            }
185
            
186
            throw $e;
187
        }
188
    }
189
    
190
    /**
191
     * @param string  $name
192
     * @return mixed
193
     * @throws \RuntimeException
194
     * @internal
195
     */
196
    function __get($name) {
197
        if(\property_exists($this, $name)) {
198
            return $this->$name;
199
        }
200
        
201
        switch($name) {
202
            case 'type':
203
                return $this->client->registry->types->get($this->typeID);
204
            break;
205
        }
206
        
207
        throw new \RuntimeException('Unknown property '.\get_class($this).'::$'.$name);
208
    }
209
    
210
    /**
211
     * @return mixed
212
     * @throws \RuntimeException
213
     * @internal
214
     */
215
    function __call($name, $args) {
216
        if(\property_exists($this, $name)) {
217
            $callable = $this->$name;
218
            if(\is_callable($callable)) {
219
                return $callable(...$args);
220
            }
221
        }
222
        
223
        throw new \RuntimeException('Unknown method '.\get_class($this).'::'.$name);
224
    }
225
    
226
    /**
227
     * @return string
228
     * @internal
229
     */
230
    function serialize() {
231
        $vars = \get_object_vars($this);
232
        
233
        unset($vars['client'], $vars['validate'], $vars['parse'], $vars['emptyChecker']);
234
        
235
        return \serialize($vars);
236
    }
237
    
238
    /**
239
     * @return void
240
     * @internal
241
     */
242
    function unserialize($vars) {
243
        if(\CharlotteDunois\Yasmin\Models\ClientBase::$serializeClient === null) {
244
            throw new \Exception('Unable to unserialize a class without ClientBase::$serializeClient being set');
245
        }
246
        
247
        $vars = \unserialize($vars);
248
        
249
        foreach($vars as $name => $val) {
250
            $this->$name = $val;
251
        }
252
        
253
        $this->client = \CharlotteDunois\Yasmin\Models\ClientBase::$serializeClient;
254
    }
255
    
256
    /**
257
     * Prompts the user and obtains the value for the argument. Resolves with an array of ('value' => mixed, 'cancelled' => string|null, 'prompts' => Message[], 'answers' => Message[]). Cancelled can be one of user, time and promptLimit.
258
     * @param \CharlotteDunois\Livia\CommandMessage         $message  Message that triggered the command.
259
     * @param string|string[]                               $value    Pre-provided value(s).
260
     * @param \CharlotteDunois\Livia\Arguments\ArgumentBag  $bag      The argument bag.
261
     * @param bool|string|null                              $valid    Whether the last retrieved value was valid.
262
     * @return \React\Promise\ExtendedPromiseInterface
263
     */
264
    function obtain(\CharlotteDunois\Livia\CommandMessage $message, $value, \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, $valid = null) {
265
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) use ($message, $value, $bag, $valid) {
266
            $empty = ($this->emptyChecker !== null ? $this->emptyChecker($value, $message, $this) : ($this->type !== null ? $this->type->isEmpty($value, $message, $this) : $value === null));
267
            if($empty && $this->default !== null) {
268
                $bag->values[] = $this->default;
269
                return $resolve($bag->done());
270
            }
271
            
272
            if($this->infinite) {
273
                if(!$empty && $value !== null) {
274
                    $this->parseInfiniteProvided($message, (\is_array($value) ? $value : array($value)), $bag)->done($resolve, $reject);
275
                    return;
276
                }
277
                
278
                $this->obtainInfinite($message, array(), $bag)->done($resolve, $reject);
279
                return;
280
            }
281
            
282
            if(!$empty && $valid === null) {
283
                $value = \trim($value);
284
                $validate = ($this->validate ? array($this, 'validate') : array($this->type, 'validate'))($value, $message, $this);
285
                if(!($validate instanceof \React\Promise\PromiseInterface)) {
286
                    $validate = \React\Promise\resolve($validate);
287
                }
288
                
289
                return $validate->then(function ($valid) use ($message, $value, $bag) {
290
                    if($valid !== true) {
291
                        return $this->obtain($message, $value, $bag, $valid);
292
                    }
293
                    
294
                    $parse = ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $message, $this);
295
                    if(!($parse instanceof \React\Promise\PromiseInterface)) {
296
                        $parse = \React\Promise\resolve($parse);
297
                    }
298
                    
299
                    return $parse->then(function ($value) use ($bag) {
300
                        $bag->values[] = $value;
301
                        return $bag->done();
302
                    });
303
                })->done($resolve, $reject);
304
            }
305
            
306
            if(\count($bag->prompts) > $bag->promptLimit) {
307
                $bag->cancelled = 'promptLimit';
308
                return $bag->done();
309
            }
310
            
311
            if($empty && $value === null) {
312
                $reply = $message->reply($this->prompt.\PHP_EOL.
313
                    'Respond with `cancel` to cancel the command. The command will automatically be cancelled in '.$this->wait.' seconds.');
314
            } elseif($valid === false) {
315
                $reply = $message->reply('You provided an invalid '.$this->label.'.'.\PHP_EOL.
316
                    'Please try again. Respond with `cancel` to cancel the command. The command will automatically be cancelled in '.$this->wait.' seconds.');
317
            } elseif(\is_string($valid)) {
318
                $reply = $message->reply($valid.\PHP_EOL.
319
                    'Please try again. Respond with `cancel` to cancel the command. The command will automatically be cancelled in '.$this->wait.' seconds.');
320
            } else {
321
                $reply = \React\Promise\resolve(null);
322
            }
323
            
324
            // Prompt the user for a new value
325
            $reply->done(function ($msg) use ($message, $bag, $resolve, $reject) {
326
                if($msg !== null) {
327
                    $bag->prompts[] = $msg;
328
                }
329
                
330
                // Get the user's response
331
                $message->message->channel->collectMessages(function ($msg) use ($message) {
332
                    return ($msg->author->id === $message->message->author->id);
333
                }, array(
334
                    'max' => 1,
335
                    'time' => $this->wait
336
                ))->then(function ($messages) use ($message, $bag) {
337
                    if($messages->count() === 0) {
338
                        $bag->cancelled = 'time';
339
                        return $bag->done();
340
                    }
341
                    
342
                    $msg = $messages->first();
343
                    $bag->answers[] = $msg;
344
                    
345
                    $value = $msg->content;
346
                    
347
                    if(\mb_strtolower($value) === 'cancel') {
348
                        $bag->cancelled = 'user';
349
                        return $bag->done();
350
                    }
351
                    
352
                    $validate = ($this->validate ? array($this, 'validate') : array($this->type, 'validate'))($value, $message, $this);
353
                    if(!($validate instanceof \React\Promise\PromiseInterface)) {
354
                        $validate = \React\Promise\resolve($validate);
355
                    }
356
                    
357
                    return $validate->then(function ($valid) use ($message, $value, $bag) {
358
                        if($valid !== true) {
359
                            return $this->obtain($message, $value, $bag, $valid);
360
                        }
361
                        
362
                        $parse = ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $message, $this);
363
                        if(!($parse instanceof \React\Promise\PromiseInterface)) {
364
                            $parse = \React\Promise\resolve($parse);
365
                        }
366
                        
367
                        return $parse->then(function ($value) use ($bag) {
368
                            $bag->values[] = $value;
369
                            return $bag->done();
370
                        });
371
                    });
372
                }, function ($error) use ($bag) {
373
                    if($error instanceof \RangeException) {
374
                        $bag->cancelled = 'time';
375
                        return $bag->done();
376
                    }
377
                    
378
                    throw $error;
379
                })->done($resolve, $reject);
380
            }, $reject);
381
        }));
382
    }
383
    
384
    /**
385
     * Prompts the user infinitely and obtains the values for the argument. Resolves with an array of ('values' => mixed, 'cancelled' => string|null, 'prompts' => Message[], 'answers' => Message[]). Cancelled can be one of user, time and promptLimit.
386
     * @param \CharlotteDunois\Livia\CommandMessage         $message      Message that triggered the command.
387
     * @param string[]                                      $values       Pre-provided values.
388
     * @param \CharlotteDunois\Livia\Arguments\ArgumentBag  $bag          The argument bag.
389
     * @param bool|string|null                              $valid        Whether the last retrieved value was valid.
390
     * @return \React\Promise\ExtendedPromiseInterface
391
     */
392
    protected function obtainInfinite(\CharlotteDunois\Livia\CommandMessage $message, array $values = array(), \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, bool $valid = null) {
393
        $value = null;
394
        if(!empty($values)) {
395
            $value = \array_shift($values);
396
        }
397
        
398
        return $this->infiniteObtain($message, $value, $bag, $valid)->then(function ($value) use ($message, &$values, $bag) {
399
            if($value instanceof \CharlotteDunois\Livia\Arguments\ArgumentBag && $value->done) {
400
                return $value;
401
            }
402
            
403
            $bag->values[] = $value;
404
            return $this->obtainInfinite($message, $values, $bag);
405
        });
406
    }
407
    
408
    /**
409
     * @return \React\Promise\ExtendedPromiseInterface
410
     */
411
    protected function infiniteObtain(\CharlotteDunois\Livia\CommandMessage $message, $value, \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, $valid = null) {
412
        if($value === null) {
413
            $reply = $message->reply($this->prompt.\PHP_EOL.
414
                'Respond with `cancel` to cancel the command, or `finish` to finish entry up to this point.'.\PHP_EOL.
415
                'The command will automatically be cancelled in '.$this->wait.' seconds.');
416
        } elseif($valid === false) {
417
            $escaped = \str_replace('@', "@\u{200B}", \CharlotteDunois\Yasmin\Utils\MessageHelpers::escapeMarkdown($value));
418
            
419
            $reply = $message->reply('You provided an invalid '.$this->label.', "'.(\mb_strlen($escaped) < 1850 ? $escaped : '[too long to show]').'". '.
420
                                        'Please try again.');
421
        } elseif(\is_string($valid)) {
422
            $reply = $message->reply($valid.\PHP_EOL.
423
                'Respond with `cancel` to cancel the command, or `finish` to finish entry up to this point.'.\PHP_EOL.
424
                'The command will automatically be cancelled in '.$this->wait.' seconds.');
425
        } else {
426
            $reply = \React\Promise\resolve(null);
427
        }
428
        
429
        return $reply->then(function ($msg) use ($message, $bag) {
430
            if($msg !== null) {
431
                $bag->prompts[] = $msg;
432
            }
433
            
434
            if(\count($bag->prompts) > $bag->promptLimit) {
435
                $bag->cancelled = 'promptLimit';
436
                return $bag->done();
437
            }
438
            
439
            // Get the user's response
440
            return $message->message->channel->collectMessages(function ($msg) use ($message) {
441
                return ($msg->author->id === $message->message->author->id);
442
            }, array(
443
                'max' => 1,
444
                'time' => $this->wait
445
            ))->then(function ($messages) use ($message, $bag) {
446
                if($messages->count() === 0) {
447
                    $bag->cancelled = 'time';
448
                    return $bag->done();
449
                }
450
                
451
                $msg = $messages->first();
452
                $bag->answers[] = $msg;
453
                
454
                $value = $msg->content;
455
                
456
                if(\mb_strtolower($value) === 'finish') {
457
                    $bag->cancelled = (\count($bag->values) > 0 ? null : 'user');
458
                    return $bag->done();
459
                } elseif(\mb_strtolower($value) === 'cancel') {
460
                    $bag->cancelled = 'user';
461
                    return $bag->done();
462
                }
463
                
464
                $validate = ($this->validate ? array($this, 'validate') : array($this->type, 'validate'))($value, $message, $this);
465
                if(!($validate instanceof \React\Promise\PromiseInterface)) {
466
                    $validate = \React\Promise\resolve($validate);
467
                }
468
                
469
                return $validate->then(function ($valid) use ($message, $value, $bag) {
470
                    if($valid !== true) {
471
                        return $this->infiniteObtain($message, $value, $bag, $valid);
472
                    }
473
                    
474
                    return ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $message, $this);
475
                });
476
            }, function ($error) use ($bag) {
477
                if($error instanceof \RangeException) {
478
                    $bag->cancelled = 'time';
479
                    return $bag->done();
480
                }
481
                
482
                throw $error;
483
            });
484
        });
485
    }
486
    
487
    /**
488
     * Parses the provided infinite arguments.
489
     * @param \CharlotteDunois\Livia\CommandMessage         $message      Message that triggered the command.
490
     * @param string[]                                      $values       Pre-provided values.
491
     * @param \CharlotteDunois\Livia\Arguments\ArgumentBag  $bag          The argument bag.
492
     * @param int                                           $i            Current index of current argument value.
493
     * @return \React\Promise\ExtendedPromiseInterface
494
     */
495
    protected function parseInfiniteProvided(\CharlotteDunois\Livia\CommandMessage $message, array $values = array(), \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, int $i = 0) {
496
        if(empty($values)) {
497
            return $this->obtainInfinite($message, array(), $bag);
498
        }
499
        
500
        return (new \React\Promise\Promise(function (callable $resolve, callable $reject) use ($message, &$values, $bag, $i) {
501
            $value = $values[$i];
502
            $val = null;
503
            
504
            $validate = ($this->validate ? array($this, 'validate') : array($this->type, 'validate'))($value, $message, $this);
505
            if(!($validate instanceof \React\Promise\PromiseInterface)) {
506
                $validate = \React\Promise\resolve($validate);
507
            }
508
            
509
            return $validate->then(function ($valid) use ($message, $value, $bag, &$val) {
510
                if($valid !== true) {
511
                    $val = $valid;
512
                    return $this->obtainInfinite($message, array($value), $bag, $val);
513
                }
514
                
515
                return ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $message, $this);
516
            })->then(function ($value) use ($message, &$values, $bag, $i, &$val) {
517
                if($val !== null) {
518
                    return $value;
519
                }
520
                
521
                $bag->values[] = $value;
522
                $i++;
523
                
524
                if($i < \count($values)) {
525
                    return $this->parseInfiniteProvided($message, $values, $bag, $i);
526
                }
527
                
528
                return $bag->done();
529
            })->done($resolve, $reject);
530
        }));
531
    }
532
}
533