| Total Complexity | 81 |
| Total Lines | 504 |
| Duplicated Lines | 0 % |
| Changes | 8 | ||
| Bugs | 0 | Features | 0 |
Complex classes like Argument often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Argument, and based on these observations, apply Extract Interface, too.
| 1 | <?php |
||
| 31 | class Argument implements \Serializable { |
||
| 32 | /** |
||
| 33 | * The client which initiated the instance. |
||
| 34 | * @var \CharlotteDunois\Livia\Client |
||
| 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\Client $client |
||
| 131 | * @param array $info |
||
| 132 | * @throws \InvalidArgumentException |
||
| 133 | */ |
||
| 134 | function __construct(\CharlotteDunois\Livia\Client $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\Commands\Context $context 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\Commands\Context $context, $value, \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, $valid = null) { |
||
| 265 | return (new \React\Promise\Promise(function (callable $resolve, callable $reject) use ($context, $value, $bag, $valid) { |
||
| 266 | $empty = ($this->emptyChecker !== null ? $this->emptyChecker($value, $context, $this) : ($this->type !== null ? $this->type->isEmpty($value, $context, $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($context, (\is_array($value) ? $value : array($value)), $bag)->done($resolve, $reject); |
||
| 275 | return; |
||
| 276 | } |
||
| 277 | |||
| 278 | $this->obtainInfinite($context, 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, $context, $this); |
||
| 285 | if(!($validate instanceof \React\Promise\PromiseInterface)) { |
||
| 286 | $validate = \React\Promise\resolve($validate); |
||
| 287 | } |
||
| 288 | |||
| 289 | return $validate->then(function ($valid) use ($context, $value, $bag) { |
||
| 290 | if($valid !== true) { |
||
| 291 | return $this->obtain($context, $value, $bag, $valid); |
||
| 292 | } |
||
| 293 | |||
| 294 | $parse = ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $context, $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 = $context->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 = $context->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 = $context->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 ($context, $bag, $resolve, $reject) { |
||
| 326 | if($msg !== null) { |
||
| 327 | $bag->prompts[] = $msg; |
||
| 328 | } |
||
| 329 | |||
| 330 | // Get the user's response |
||
| 331 | $promise = $context->message->channel->collectMessages(function ($msg) use ($context) { |
||
| 332 | return ($msg->author->id === $context->message->author->id); |
||
| 333 | }, array( |
||
| 334 | 'max' => 1, |
||
| 335 | 'time' => $this->wait |
||
| 336 | ))->then(function ($messages) use ($context, $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, $context, $this); |
||
| 353 | if(!($validate instanceof \React\Promise\PromiseInterface)) { |
||
| 354 | $validate = \React\Promise\resolve($validate); |
||
| 355 | } |
||
| 356 | |||
| 357 | return $validate->then(function ($valid) use ($context, $value, $bag) { |
||
| 358 | if($valid !== true) { |
||
| 359 | return $this->obtain($context, $value, $bag, $valid); |
||
| 360 | } |
||
| 361 | |||
| 362 | $parse = ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $context, $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 | |||
| 381 | $this->client->dispatcher->setAwaiting($context, $promise); |
||
| 382 | }, $reject); |
||
| 383 | })); |
||
| 384 | } |
||
| 385 | |||
| 386 | /** |
||
| 387 | * 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. |
||
| 388 | * @param \CharlotteDunois\Livia\Commands\Context $context Message that triggered the command. |
||
| 389 | * @param string[] $values Pre-provided values. |
||
| 390 | * @param \CharlotteDunois\Livia\Arguments\ArgumentBag $bag The argument bag. |
||
| 391 | * @param bool|string|null $valid Whether the last retrieved value was valid. |
||
| 392 | * @return \React\Promise\ExtendedPromiseInterface |
||
| 393 | */ |
||
| 394 | protected function obtainInfinite(\CharlotteDunois\Livia\Commands\Context $context, array $values = array(), \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, bool $valid = null) { |
||
| 407 | }); |
||
| 408 | } |
||
| 409 | |||
| 410 | /** |
||
| 411 | * @return \React\Promise\ExtendedPromiseInterface |
||
| 412 | */ |
||
| 413 | protected function infiniteObtain(\CharlotteDunois\Livia\Commands\Context $context, $value, \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, $valid = null) { |
||
| 414 | if($value === null) { |
||
| 415 | $reply = $context->reply($this->prompt.\PHP_EOL. |
||
| 416 | 'Respond with `cancel` to cancel the command, or `finish` to finish entry up to this point.'.\PHP_EOL. |
||
| 417 | 'The command will automatically be cancelled in '.$this->wait.' seconds.'); |
||
| 418 | } elseif($valid === false) { |
||
| 419 | $escaped = \str_replace('@', "@\u{200B}", \CharlotteDunois\Yasmin\Utils\MessageHelpers::escapeMarkdown($value)); |
||
| 420 | |||
| 421 | $reply = $context->reply('You provided an invalid '.$this->label.', "'.(\mb_strlen($escaped) < 1850 ? $escaped : '[too long to show]').'". '. |
||
| 422 | 'Please try again.'); |
||
| 423 | } elseif(\is_string($valid)) { |
||
| 424 | $reply = $context->reply($valid.\PHP_EOL. |
||
| 425 | 'Respond with `cancel` to cancel the command, or `finish` to finish entry up to this point.'.\PHP_EOL. |
||
| 426 | 'The command will automatically be cancelled in '.$this->wait.' seconds.'); |
||
| 427 | } else { |
||
| 428 | $reply = \React\Promise\resolve(null); |
||
| 429 | } |
||
| 430 | |||
| 431 | return $reply->then(function ($msg) use ($context, $bag) { |
||
| 432 | if($msg !== null) { |
||
| 433 | $bag->prompts[] = $msg; |
||
| 434 | } |
||
| 435 | |||
| 436 | if(\count($bag->prompts) > $bag->promptLimit) { |
||
| 437 | $bag->cancelled = 'promptLimit'; |
||
| 438 | return $bag->done(); |
||
| 439 | } |
||
| 440 | |||
| 441 | // Get the user's response |
||
| 442 | $promise = $context->message->channel->collectMessages(function ($msg) use ($context) { |
||
| 443 | return ($msg->author->id === $context->message->author->id); |
||
| 444 | }, array( |
||
| 445 | 'max' => 1, |
||
| 446 | 'time' => $this->wait |
||
| 447 | ))->then(function ($contexts) use ($context, $bag) { |
||
| 448 | if($contexts->count() === 0) { |
||
| 449 | $bag->cancelled = 'time'; |
||
| 450 | return $bag->done(); |
||
| 451 | } |
||
| 452 | |||
| 453 | $msg = $contexts->first(); |
||
| 454 | $bag->answers[] = $msg; |
||
| 455 | |||
| 456 | $value = $msg->content; |
||
| 457 | |||
| 458 | if(\mb_strtolower($value) === 'finish') { |
||
| 459 | $bag->cancelled = (\count($bag->values) > 0 ? null : 'user'); |
||
| 460 | return $bag->done(); |
||
| 461 | } elseif(\mb_strtolower($value) === 'cancel') { |
||
| 462 | $bag->cancelled = 'user'; |
||
| 463 | return $bag->done(); |
||
| 464 | } |
||
| 465 | |||
| 466 | $validate = ($this->validate ? array($this, 'validate') : array($this->type, 'validate'))($value, $context, $this); |
||
| 467 | if(!($validate instanceof \React\Promise\PromiseInterface)) { |
||
| 468 | $validate = \React\Promise\resolve($validate); |
||
| 469 | } |
||
| 470 | |||
| 471 | return $validate->then(function ($valid) use ($context, $value, $bag) { |
||
| 472 | if($valid !== true) { |
||
| 473 | return $this->infiniteObtain($context, $value, $bag, $valid); |
||
| 474 | } |
||
| 475 | |||
| 476 | return ($this->parse ? array($this, 'parse') : array($this->type, 'parse'))($value, $context, $this); |
||
| 477 | }); |
||
| 478 | }, function ($error) use ($bag) { |
||
| 479 | if($error instanceof \RangeException) { |
||
| 480 | $bag->cancelled = 'time'; |
||
| 481 | return $bag->done(); |
||
| 482 | } |
||
| 483 | |||
| 484 | throw $error; |
||
| 485 | }); |
||
| 486 | |||
| 487 | $this->client->dispatcher->setAwaiting($context, $promise); |
||
| 488 | return $promise; |
||
| 489 | }); |
||
| 490 | } |
||
| 491 | |||
| 492 | /** |
||
| 493 | * Parses the provided infinite arguments. |
||
| 494 | * @param \CharlotteDunois\Livia\Commands\Context $context Message that triggered the command. |
||
| 495 | * @param string[] $values Pre-provided values. |
||
| 496 | * @param \CharlotteDunois\Livia\Arguments\ArgumentBag $bag The argument bag. |
||
| 497 | * @param int $i Current index of current argument value. |
||
| 498 | * @return \React\Promise\ExtendedPromiseInterface |
||
| 499 | */ |
||
| 500 | protected function parseInfiniteProvided(\CharlotteDunois\Livia\Commands\Context $context, array $values = array(), \CharlotteDunois\Livia\Arguments\ArgumentBag $bag, int $i = 0) { |
||
| 535 | })); |
||
| 536 | } |
||
| 537 | } |
||
| 538 |