├── LICENSE ├── README.md ├── VERSION ├── bin └── release.php ├── composer.json ├── composer.lock ├── phpunit.ci.xml └── src └── Console.php /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Simon Asika 4 | 5 | Permission is hereby granted, free of charge, to any person 6 | obtaining a copy of this software and associated documentation 7 | files (the "Software"), to deal in the Software without 8 | restriction, including without limitation the rights to use, 9 | copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the 11 | Software is furnished to do so, subject to the following 12 | conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 19 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 21 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 22 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 23 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 24 | OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Simple Console V2.0 2 | 3 | ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/asika32764/php-simple-console/ci.yml?style=for-the-badge) 4 | [![Packagist Version](https://img.shields.io/packagist/v/asika/simple-console?style=for-the-badge) 5 | ](https://packagist.org/packages/asika/simple-console) 6 | [![Packagist Downloads](https://img.shields.io/packagist/dt/asika/simple-console?style=for-the-badge)](https://packagist.org/packages/asika/simple-console) 7 | 8 | Single file console framework to help you write scripts quickly, **v2.0 requires PHP 8.4 or later**. 9 | 10 | > This package is highly inspired by [Symfony Console](https://symfony.com/doc/current/components/console.html) and Python [argparse](https://docs.python.org/3/library/argparse.html). 11 | 12 | 13 | * [PHP Simple Console V2.0](#php-simple-console-v20) 14 | * [Installation](#installation) 15 | * [Getting Started](#getting-started) 16 | * [Run Console by Closure](#run-console-by-closure) 17 | * [Run Console by Custom Class](#run-console-by-custom-class) 18 | * [The Return Value](#the-return-value) 19 | * [Parameter Parser](#parameter-parser) 20 | * [Parameter Definitions](#parameter-definitions) 21 | * [Show Help](#show-help) 22 | * [Override Help Information](#override-help-information) 23 | * [Parameter Configurations](#parameter-configurations) 24 | * [Get Parameters Value](#get-parameters-value) 25 | * [Parameters Type](#parameters-type) 26 | * [`ARRAY` type](#array-type) 27 | * [`LEVEL` type](#level-type) 28 | * [Parameters Options](#parameters-options) 29 | * [`description`](#description) 30 | * [`required`](#required) 31 | * [`default`](#default) 32 | * [`negatable`](#negatable) 33 | * [Parameters Parsing](#parameters-parsing) 34 | * [Error Handling](#error-handling) 35 | * [Wrong Parameters](#wrong-parameters) 36 | * [Verbosity](#verbosity) 37 | * [The Built-In Options](#the-built-in-options) 38 | * [Disable Built-In Options for Console App](#disable-built-in-options-for-console-app) 39 | * [Input/Output](#inputoutput) 40 | * [STDIN/STDOUT/STDERR](#stdinstdoutstderr) 41 | * [Output Methods](#output-methods) 42 | * [Input and Asking Questions](#input-and-asking-questions) 43 | * [Run Sub-Process](#run-sub-process) 44 | * [Hide Command Name](#hide-command-name) 45 | * [Custom Output](#custom-output) 46 | * [Disable the Output](#disable-the-output) 47 | * [Override `exec()`](#override-exec) 48 | * [Delegating Multiple Tasks](#delegating-multiple-tasks) 49 | * [Contributing and PR is Welcome](#contributing-and-pr-is-welcome) 50 | 51 | 52 | ## Installation 53 | 54 | Use composer: 55 | 56 | ``` bash 57 | composer require asika/simple-console 58 | ``` 59 | 60 | Download single 61 | file: [Download Here](https://raw.githubusercontent.com/asika32764/php-simple-console/master/src/Console.php) 62 | 63 | CLI quick download: 64 | 65 | ```shell 66 | # WGET 67 | wget https://raw.githubusercontent.com/asika32764/php-simple-console/master/src/Console.php 68 | chmod +x Console.php 69 | 70 | # CURL 71 | curl https://raw.githubusercontent.com/asika32764/php-simple-console/master/src/Console.php -o Console.php 72 | chmod +x Console.php 73 | ``` 74 | 75 | ## Getting Started 76 | 77 | ### Run Console by Closure 78 | 79 | Use closure to create a simple console script. 80 | 81 | ```php 82 | #!/bin/sh php 83 | execute( 93 | $argv, 94 | function () use ($app) { 95 | $this->writeln('Hello'); 96 | 97 | // OR 98 | 99 | $app->writeln('Hello'); 100 | 101 | return $app::SUCCESS; // OR `0` as success 102 | } 103 | ); 104 | ``` 105 | 106 | The closure can receive a `Console` object as parameter, so you can use `$this` or `$app` to access the console object. 107 | 108 | ```php 109 | $app->execute( 110 | $argv, 111 | function (Console $app) { 112 | $app->writeln('Hello'); 113 | 114 | // ... 115 | } 116 | ); 117 | ``` 118 | 119 | The `$argv` can be omit, Console will get it from `$_SERVER['argv']` 120 | 121 | ```php 122 | $app->execute( 123 | main: function (Console $app) { 124 | $app->writeln('Hello'); 125 | 126 | // ... 127 | } 128 | ); 129 | ``` 130 | 131 | ### Run Console by Custom Class 132 | 133 | You can also use class mode, extends `Asika\SimpleConsole\Console` class to create a console application. 134 | 135 | ```php 136 | $app = new class () extends \Asika\SimpleConsole\Console 137 | { 138 | protected function doExecute(): int|bool 139 | { 140 | $this->writeln('Hello'); 141 | 142 | return static::SUCCESS; 143 | } 144 | } 145 | 146 | $app->execute(); 147 | 148 | // OR 149 | 150 | $app->execute($argv); 151 | ``` 152 | 153 | ### The Return Value 154 | 155 | You can return `true` or `0` as success, `false` or any int larger than `0` as failure. Please refer to 156 | [GNU/Linux Exit Codes](https://slg.ddnss.de/list-of-common-exit-codes-for-gnu-linux/). 157 | 158 | Simple Console provides constants for success and failure: 159 | 160 | ```php 161 | retrun $app::SUCCESS; // 0 162 | retrun $app::FAILURE; // 255 163 | ``` 164 | 165 | ## Parameter Parser 166 | 167 | Simple Console supports arguments and options pre-defined. Below is a parser object to help use define parameters and 168 | parse `argv` variables. You can pass the parsed data to any function or entry point. 169 | 170 | ```php 171 | use Asika\SimpleConsole\Console; 172 | 173 | function main(array $options) { 174 | // Run your code... 175 | } 176 | 177 | $parser = \Asika\SimpleConsole\Console::createArgvParser(); 178 | 179 | // Arguments 180 | $parser->addParameter('name', type: Console::STRING, description: 'Your name', required: true); 181 | $parser->addParameter('age', type: Console::INT, description: 'Your age'); 182 | 183 | // Name starts with `-` or `--` will be treated as option 184 | $parser->addParameter('--height|-h', type: Console::FLOAT, description: 'Your height', required: true); 185 | $parser->addParameter('--location|-l', type: Console::STRING, description: 'Live location', required: true); 186 | $parser->addParameter('--muted|-m', type: Console::BOOLEAN, description: 'Is muted'); 187 | 188 | main($parser->parse($argv)); 189 | ``` 190 | 191 | Same as: 192 | 193 | ```php 194 | use Asika\SimpleConsole\ArgvParser; 195 | use Asika\SimpleConsole\Console; 196 | 197 | $params = Console::parseArgv( 198 | function (ArgvParser $parser) { 199 | // Arguments 200 | $parser->addParameter('name', type: Console::STRING, description: 'Your name', required: true); 201 | $parser->addParameter('age', type: Console::INT, description: 'Your age'); 202 | 203 | // Name starts with `-` or `--` will be treated as option 204 | $parser->addParameter('--height|-h', type: Console::FLOAT, description: 'Your height', required: true); 205 | $parser->addParameter('--location|-l', type: Console::STRING, description: 'Live location', required: true); 206 | $parser->addParameter('--muted|-m', type: Console::BOOLEAN, description: 'Is muted'); 207 | }, 208 | // pass $argv or leave empty 209 | ); 210 | 211 | main($params); 212 | ``` 213 | 214 | ## Parameter Definitions 215 | 216 | After upgraded to 2.0, the parameter defining is required for `Console` app, if a provided argument or option is not 217 | exists 218 | in pre-defined parameters, it will raise an error. 219 | 220 | ```php 221 | // me.php 222 | 223 | $app = new Console(); 224 | // Arguments 225 | $app->addParameter('name', type: $app::STRING, description: 'Your name', required: true); 226 | $app->addParameter('age', type: $app::INT, description: 'Your age'); 227 | 228 | // Name starts with `-` or `--` will be treated as option 229 | $app->addParameter('--height', type: $app::FLOAT, description: 'Your height', required: true); 230 | $app->addParameter('--location|-l', type: $app::STRING, description: 'Live location', required: true); 231 | $app->addParameter('--muted|-m', type: $app::BOOLEAN, description: 'Is muted'); 232 | 233 | $app->execute( 234 | argv: $argv, 235 | main: function () use ($app) { 236 | $app->writeln('Hello'); 237 | $app->writeln('Name: ' . $app->get('name')); 238 | $app->writeln('Age: ' . $app->get('age')); 239 | $app->writeln('Height: ' . $app->get('height')); 240 | $app->writeln('Location: ' . $app->get('location')); 241 | $app->writeln('Muted: ' . $app->get('muted') ? 'Y' : 'N'); 242 | 243 | return $app::SUCCESS; 244 | } 245 | ); 246 | ``` 247 | 248 | Also same as: 249 | 250 | ```php 251 | // me.php 252 | 253 | $app = new class () extends Console 254 | { 255 | protected function configure(): void 256 | { 257 | // Arguments 258 | $this->addParameter('name', type: $this::STRING, description: 'Your name', required: true); 259 | $this->addParameter('age', type: $this::INT, description: 'Your age'); 260 | 261 | // Name starts with `-` or `--` will be treated as option 262 | $this->addParameter('--height', type: $this::FLOAT, description: 'Your height', required: true); 263 | $this->addParameter('--location|-l', type: $this::STRING, description: 'Live location', required: true); 264 | $this->addParameter('--muted|-m', type: $this::BOOLEAN, description: 'Is muted'); 265 | } 266 | 267 | protected function doExecute(): int|bool 268 | { 269 | $this->writeln('Hello'); 270 | $this->writeln('Name: ' . $this->get('name')); 271 | $this->writeln('Age: ' . $this->get('age')); 272 | $this->writeln('Height: ' . $this->get('height')); 273 | $this->writeln('Location: ' . $this->get('location')); 274 | $this->writeln('Muted: ' . ($this->get('muted') ? 'Y' : 'N')); 275 | 276 | return $this::SUCCESS; 277 | } 278 | }; 279 | $app->execute(); 280 | ``` 281 | 282 | Now if we enter 283 | 284 | ```bash 285 | php me.php --name="John Doe " --age=18 --height=1.8 --location=America --muted 286 | ``` 287 | 288 | It shows: 289 | 290 | ``` 291 | Hello 292 | Name: John Doe 293 | Age: 25 294 | Height: 1.8 295 | Location: America 296 | Muted: Y 297 | ``` 298 | 299 | Then, if we enter wrong parameters, Simple Console will throw errors: 300 | 301 | ```bash 302 | php me.php # [Warning] Required argument "name" is missing. 303 | php me.php Simon eighteen # [Warning] Invalid value type for "age". Expected INT. 304 | php me.php Simon 18 foo bar # [Warning] Unknown argument "foo". 305 | php me.php Simon 18 --nonexists # [Warning] The "-nonexists" option does not exist. 306 | php me.php Simon 18 --location # [Warning] Required value for "location" is missing. 307 | php me.php Simon 18 --muted=ON # [Warning] Option "muted" does not accept value. 308 | php me.php Simon 18 --height one-point-eight # [Warning] Invalid value type for "height". Expected FLOAT. 309 | ``` 310 | 311 | ### Show Help 312 | 313 | Simple Console supports to describe arguments/options information which follows [docopt](http://docopt.org/) standards. 314 | 315 | Add `--help` or `-h` to Console App: 316 | 317 | ```bash 318 | php me.php --help 319 | ``` 320 | 321 | Will print the help information: 322 | 323 | ``` 324 | Usage: 325 | me.php [options] [--] [] 326 | 327 | Arguments: 328 | name Your name 329 | age Your age 330 | 331 | Options: 332 | --height=HEIGHT Your height 333 | -l, --location=LOCATION Live location 334 | -m, --muted Is muted 335 | -h, --help Show description of all parameters 336 | -v, --verbosity The verbosity level of the output 337 | 338 | ``` 339 | 340 | Add your heading/epilog and command name: 341 | 342 | ```php 343 | // Use constructor 344 | $app = new \Asika\SimpleConsole\Console( 345 | heading: <<
heading = <<
commandName = 'show-me.php'; // If not provided, will auto use script file name 368 | $app->epilog = <<execute(); 376 | ``` 377 | 378 | The result: 379 | 380 | ``` 381 | [Console] SHOW ME - v1.0 382 | 383 | This command can show personal information. 384 | 385 | Usage: 386 | show-me.php [options] [--] [] 387 | 388 | Arguments: 389 | name Your name 390 | age Your age 391 | 392 | Options: 393 | --height=HEIGHT Your height 394 | -l, --location=LOCATION Live location 395 | -m, --muted Is muted 396 | -h, --help Show description of all parameters 397 | -v, --verbosity The verbosity level of the output 398 | 399 | Help: 400 | $ show-me.php John 18 401 | $ show-me.php John 18 --location=Europe --height 1.75 402 | 403 | ...more please see https://show-me.example 404 | ``` 405 | 406 | ### Override Help Information 407 | 408 | If your are using class extending, you may override `showHelp()` method to add your own help information. 409 | 410 | ```php 411 | $app = new class () extends Console 412 | { 413 | public function showHelp(): void 414 | { 415 | $this->writeln( 416 | <<addParameter('name', type: $app::STRING, description: 'Your name', required: true); 437 | 438 | // Chaining style 439 | $app->addParameter('name', type: $app::STRING) 440 | ->description('Your name') 441 | ->required(true) 442 | ->default('John Doe'); 443 | ``` 444 | 445 | The parameter name starts with `-` and `--` will be treated as options, you can use `|` to separate primary name 446 | and shortcuts. 447 | 448 | ```php 449 | $app->addParameter('--foo', type: $app::STRING, description: 'Foo description'); 450 | $app->addParameter('--muted|-m', type: $app::BOOLEAN, description: 'Muted description'); 451 | ``` 452 | 453 | Arguments' name cannot same as options' name, otherwise it will throw an error. 454 | 455 | ### Get Parameters Value 456 | 457 | To get parameters' value, you can use `get()` method, all values will cast to the type which you defined. 458 | 459 | ```php 460 | $name = $app->get('name'); // String 461 | $height = $app->get('height'); // Int 462 | $muted = $app->get('muted'); // Bool 463 | ``` 464 | 465 | Array access also works: 466 | 467 | ```php 468 | $name = $app['name']; 469 | $height = $app['height']; 470 | ``` 471 | 472 | If a parameter is not provided, it will return `FALSE`, and if a parameter provided but has no value, it will 473 | return as `NULL`. 474 | 475 | ```bash 476 | php console.php # `dir` is FALSE 477 | php console.php --dir # `dir` is NULL 478 | php console.php --dir /path/to/dir # `dir` is `/path/to/dir` 479 | ``` 480 | 481 | So you can easily detect the parameter existence and give a default value. 482 | 483 | ```php 484 | if (($dir = $app['dir']) !== false) { 485 | $dir ??= '/path/to/default'; 486 | } 487 | ``` 488 | 489 | ### Parameters Type 490 | 491 | You can define the type of parameters, it will auto convert to the type you defined. 492 | 493 | | Type | Argument | Option | Description | 494 | |---------|------------------------------|-----------------------|-------------------------------------------------------------------------| 495 | | STRING | String type | String type | | 496 | | INT | Integer type | Integer type | | 497 | | FLOAT | Float type | Flot type | Can be int or float, will all converts to float | 498 | | NUMERIC | Int or Float | Int or Float | Can be int or float, will all converts to float | 499 | | BOOLEAN | (X) | Add `--opt` as `TRUE` | Use `negatable` to supports `--opt` as `TRUE` and `--no-opt` as `FALSE` | 500 | | ARRAY | Array, must be last argument | Array | Can provide multiple as `string[]` | 501 | | LEVEL | (X) | Int type | Can provide multiple times and convert the times to int | 502 | 503 | All parameter values parsed from `argv` is default as `string` type, and convert to the type you defined. 504 | 505 | #### `ARRAY` type 506 | 507 | The `ARRAY` can be use to arguments and options. 508 | 509 | If you set an argument as `ARRAY` type, it must be last argument, and you can add more tailing arguments. 510 | 511 | ```php 512 | $app->addParameter('name', $app::STRING); 513 | $app->addParameter('tag', $app::ARRAY); 514 | 515 | // Run: console.php foo a b c d e 516 | 517 | $app->get('tag'); // [a, b, c ,d, e] 518 | ``` 519 | 520 | Use `--` to escape all following options, all will be treated as arguments, it is useful if you are 521 | writing a proxy script. 522 | 523 | ```bash 524 | php listen.php --timeout 500 --wait 100 -- php flower.php hello --name=sakura --location Japan --muted 525 | 526 | // The last argument values will be: 527 | // ['php', 'flower.php', 'hello', '--name=sakura', '--location', 'Japan', '--muted'] 528 | ``` 529 | 530 | If you set an option as `ARRAY` type, it can be used as `--tag a --tag b --tag c`. 531 | 532 | ```php 533 | $app->addParameter('--tag|-t', $app::ARRAY); 534 | 535 | $app->get('tag'); // [a, b, c] 536 | ``` 537 | 538 | #### `LEVEL` type 539 | 540 | The `LEVEL` type is a special type, it will convert the times to int. For example, a verbosity level of `-vvv` will be 541 | converted to `3`, 542 | and `-v` will be converted to `1`. You can use this type to define the verbosity level of your argv parser. 543 | 544 | ```php 545 | $parser->addParameter('--verbosity|-v', type: $app::LEVEL, description: 'The verbosity level of the output'); 546 | ``` 547 | 548 | If you are using `Console` class, the verbosity is built-in option, you don't need to define it again. 549 | 550 | ### Parameters Options 551 | 552 | #### `description` 553 | 554 | - **Argument**: Description for the argument, will be shown in help information. 555 | - **Option**: Description for the option, will be shown in help information. 556 | 557 | #### `required` 558 | 559 | - **Argument**: Required argument must be provided, otherwise it will throw an error. 560 | - You should not set a required argument after an optional argument. 561 | - **Option**: All options are `optional`. 562 | - If you set an option as `required`, it means this option requires a value, only `--option` without value is not 563 | allowed. 564 | - `boolean` option should not be required. 565 | 566 | #### `default` 567 | 568 | - **Argument**: Default value for the argument. 569 | - If not provided, it will be `null`, `false` for boolean type, or `[]` for array type. 570 | - **Option**: Default value for the option. 571 | - If not provided, it will be `null`, `false` for boolean type, or `[]` for array type. 572 | 573 | #### `negatable` 574 | 575 | - **Argument**: Argument cannot use this option. 576 | - **Option**: Negatable option. Should work with `boolean` type. 577 | - If set to `true`, it will support `--xxx|--no-xxx` 2 styles to set `true|false`. 578 | - If you want to set a boolean option's default as `TRUE` and use `--no-xxx` to set it as `FALSE`, you can do this: 579 | ```php 580 | $app->addParameter('--muted|-m', $app::BOOLEAN, default: true, negatable: true); 581 | ``` 582 | 583 | ### Parameters Parsing 584 | 585 | Simple Console follows [docopt](http://docopt.org/) style to parse parameters. 586 | 587 | - Long options starts with `--` 588 | - Option shortcut starts with `-` 589 | - If an option `-a` requires value, `-abc` will parse as `$a = bc` 590 | - If option `-a` dose not require value, `-abc` will parse as `$a = true, $b = true, $c = true` 591 | - Long options supports `=` while shortcuts are not. The `--foo=bar` is valid and `-f=bar` is invalid, you should use 592 | `-f bar`. 593 | - Add `--` to escape all following options, all will be treated as arguments. 594 | 595 | ## Error Handling 596 | 597 | Just throw Exception in `doExecute()`, Console will auto catch error. 598 | 599 | ``` php 600 | throw new \RuntimeException('An error occurred'); 601 | ``` 602 | 603 | If Console app receive an Throwable or Exception, it will render an `[ERROR]` message: 604 | 605 | ```bash 606 | [Error] An error occurred. 607 | ``` 608 | 609 | Add `-v` to show backtrace if error. 610 | 611 | ``` 612 | [Error] An error occurred. 613 | [Backtrace]: 614 | #0 /path/to/Console.php(145): Asika\SimpleConsole\Console@anonymous->doExecute() 615 | #1 /path/to/test.php(36): Asika\SimpleConsole\Console->execute() 616 | #2 {main} 617 | ``` 618 | 619 | ### Wrong Parameters 620 | 621 | If you provide wrong parameters, Console will render a `[WARNING]` message with synopsis: 622 | 623 | ``` 624 | [Warning] Invalid value type for "age". Expected INT. 625 | 626 | test.php [-h|--help] [-v|--verbosity] [--height HEIGHT] [-l|--location LOCATION] [-m|--muted] [--] [] 627 | ``` 628 | 629 | You can manually raise this `[WARNING]` by throwing `InvalidParameterException`: 630 | 631 | ```php 632 | if ($app->get('age') < 18) { 633 | throw new \Asika\SimpleConsole\InvalidParameterException('Age must greater than 18.'); 634 | } 635 | ``` 636 | 637 | ### Verbosity 638 | 639 | You can set verbosity by option `-v` 640 | 641 | ```bash 642 | php console.php # verbosity: 0 643 | php console.php -v # verbosity: 1 644 | php console.php -vv # verbosity: 2 645 | php console.php -vvv # verbosity: 3 646 | ``` 647 | 648 | or ser it manually in PHP: 649 | 650 | ```php 651 | $app->verbosity = 3; 652 | ``` 653 | 654 | If `verbosity` is larger than `0`, it will show backtrace in exception output. 655 | 656 | You can show your own debug information on different verbosity: 657 | 658 | ```php 659 | if ($app->verbosity > 2) { 660 | $app->writeln($debugInfo); 661 | } 662 | ``` 663 | 664 | ## The Built-In Options 665 | 666 | The `--help|-h` and `--verbosity|-v` options are built-in options if you use `Console` app. 667 | 668 | ```php 669 | $app = new \Asika\SimpleConsole\Console(); 670 | // add parameters... 671 | $app->execute(); // You can use built-in `-h` and `-v` options 672 | ``` 673 | 674 | If you parse `argv` by `ArgvParser`, you must add it manually. To avoid the required parameters error, 675 | you can set `validate` to `false` when parsing. Then validate and cast parameters after parsing and help 676 | content display. 677 | 678 | ```php 679 | $parser->addParameter( 680 | '--help|-h', 681 | static::BOOLEAN, 682 | 'Show description of all parameters', 683 | default: false 684 | ); 685 | 686 | $parser->addParameter( 687 | '--verbosity|-v', 688 | static::LEVEL, 689 | 'The verbosity level of the output', 690 | ); 691 | 692 | // Add other parameters... 693 | 694 | /** @var \Asika\SimpleConsole\ArgvParser $parser */ 695 | $params = $parser->parse($argv, validate: false); 696 | 697 | if ($params['help'] !== false) { 698 | echo \Asika\SimpleConsole\ParameterDescriptor::describe($parser, 'command.php'); 699 | exit(0); 700 | } 701 | 702 | // Now we can validate and cast params 703 | $params = $parser->validateAndCastParams($params); 704 | 705 | main($params); 706 | ``` 707 | 708 | ### Disable Built-In Options for Console App 709 | 710 | If you don't want to use built-in options for Console App, you can set `disableDefaultParameters` to `true`: 711 | 712 | ```php 713 | $app = new \Asika\SimpleConsole\Console(); 714 | $app->disableDefaultParameters = true; 715 | 716 | // Add it manually 717 | $app->addParameter('--help|-h', $app::BOOLEAN, default: false); 718 | 719 | // Set verbosity 720 | $app->verbosity = (int) env('DEBUG_LEVEL'); 721 | 722 | $app->execute( 723 | main: function (\Asika\SimpleConsole\Console $app) { 724 | if ($app->get('help')) { 725 | $this->showHelp(); 726 | return 0; 727 | } 728 | 729 | // ... 730 | } 731 | ); 732 | ``` 733 | 734 | ## Input/Output 735 | 736 | ### STDIN/STDOUT/STDERR 737 | 738 | Simple Console supports to read from STDIN and write to STDOUT/STDERR. The default stream can be set at constructor. 739 | 740 | ```php 741 | new Console( 742 | stdout: STDOUT, 743 | stderr: STDERR, 744 | stdin: STDIN 745 | ); 746 | ``` 747 | 748 | If you want to catch the output, you can set `stdout` to a file pointer or a stream. 749 | 750 | ```php 751 | $fp = fopen('php://memory', 'r+'); 752 | 753 | $app = new Console(stdout: $fp); 754 | $app->execute(); 755 | 756 | rewind($fp); 757 | echo stream_get_contents($fp); 758 | ``` 759 | 760 | ### Output Methods 761 | 762 | To output messages, you can use these methods: 763 | 764 | - `write(string $message, bool $err = false)`: Write to STDOUT or STDERR 765 | - `writeln(string $message, bool $err = false)`: Write to STDOUT or STDERR with a new line 766 | - `newLine(int $lines, bool $err = false)`: Write empty new lines to STDOUT or STDERR 767 | 768 | ### Input and Asking Questions 769 | 770 | To input data, you can use `in()` methods: 771 | 772 | ```php 773 | // This will wait user enter text... 774 | $app->write('Please enter something: '); 775 | $ans = $app->in(); 776 | ``` 777 | 778 | Use `ask()` to ask a question, if return empty string, the default value will instead. 779 | 780 | ```php 781 | $ans = $app->ask('What is your name: ', [$default]); 782 | ``` 783 | 784 | Use `askConfirm()` to ask a question with `Y/n`: 785 | 786 | ```php 787 | $ans = $app->askConfirm('Are you sure you want to do this? [y/N]: '); // Return BOOL 788 | 789 | // Set default as Yes 790 | $ans = $app->askConfirm('Are you sure you want to do this? [Y/n]: ', 'y'); 791 | ``` 792 | 793 | - The `'n', 'no', 'false', 0, '0'` will be `FALSE`. 794 | - The `'y', 'yes', 'true', 1, '1'` will be `TRUE`. 795 | 796 | To add your boolean mapping, set values to `boolMapping` 797 | 798 | ```php 799 | $app->boolMapping[0] = [...]; // Falsy values 800 | $app->boolMapping[1] = [...]; // Truly values 801 | ``` 802 | 803 | ## Run Sub-Process 804 | 805 | Use `exec()` to run a sub-process, it will instantly print the output and return the result code of the command. 806 | 807 | ```php 808 | $app->exec('ls'); 809 | $app->exec('git status'); 810 | $app->exec('git commit ...'); 811 | $result = $app->exec('git push'); 812 | 813 | // All output will instantly print to STDOUT 814 | 815 | if ($result->code !== 0) { 816 | // Failure 817 | } 818 | 819 | $result->code; // 0 is success 820 | $result->success; // BOOL 821 | ``` 822 | 823 | Use `mustExec()` to make sure a sub-process should run success, otherwise it will throw an exception. 824 | 825 | ```php 826 | try { 827 | $this->mustExec('...'); 828 | } catch (\RuntimeException $e) { 829 | // 830 | } 831 | ``` 832 | 833 | ### Hide Command Name 834 | 835 | Bt default, `exec()` and `mustExec()` will show the command name before executing, for example. 836 | 837 | ```basg 838 | >> git show 839 | ... 840 | 841 | >> git commit -am "" 842 | ... 843 | 844 | >> git push 845 | ... 846 | ``` 847 | 848 | if you want to hide it, set the arg: `showCmd` to `false`. 849 | 850 | ```php 851 | $app->exec('cmd...', showCmd: false); 852 | ``` 853 | 854 | ### Custom Output 855 | 856 | Simple Console use `proc_open()` to run sub-process, so you can set your own output stream by callback. 857 | 858 | ```php 859 | $log = ''; 860 | 861 | $app->exec( 862 | 'cmd ...', 863 | output: function (string $data, bool $err) use ($app, &$log) { 864 | $app->write($data, $err); 865 | 866 | $log .= $data; 867 | } 868 | ); 869 | ``` 870 | 871 | ### Disable the Output 872 | 873 | Use `false` to disable the output, you can get full output from result object after sub-process finished. 874 | 875 | Note, the output will only write to result object if `output` set to `false`. If you set `output` as closure or 876 | keep default `NULL`, the output will be empty in result object. 877 | 878 | ```php 879 | $result = $app->exec('cmd ...', output: false); 880 | 881 | $result->output; // StdOutput of sub-process 882 | $result->errOutput; // StdErr Output of sub-process 883 | 884 | 885 | // Below will not write to the result object 886 | $result = $app->exec('cmd ...'); 887 | // OR 888 | $result = $app->exec('cmd ...', output: function () { ... }); 889 | 890 | $result->output; // Empty 891 | $result->errOutput; // Empty 892 | ``` 893 | 894 | ### Override `exec()` 895 | 896 | By now, running sub-process by `prop_open()` is in BETA, if `prop_open()` not work for your environment, simply override 897 | `exec()` to use PHP `system()` instead. 898 | 899 | ```php 900 | public function exec(string $cmd, \Closure|null $output = null, bool $showCmd = true): ExecResult 901 | { 902 | !$showCmd || $this->writeln('>> ' . $cmd); 903 | 904 | $returnLine = system($cmd, $code); 905 | 906 | return new \Asika\SimpleConsole\ExecResult($code, $returnLine, $returnLine); 907 | } 908 | ``` 909 | 910 | ## Delegating Multiple Tasks 911 | 912 | If your script has multiple tasks, for example, the build script contains `configure|make|clear` etc... 913 | 914 | Here is an example to show how to delegate multiple tasks and pass the necessary params to method interface. 915 | 916 | ```php 917 | $app = new class () extends Console 918 | { 919 | protected function configure(): void 920 | { 921 | $this->addParameter('task', type: $this::STRING, description: 'Task (configure|build|make|move|clear)', required: true); 922 | $this->addParameter('--lib-path', type: $this::STRING); 923 | $this->addParameter('--temp-path', type: $this::STRING); 924 | $this->addParameter('--nested', type: $this::STRING); 925 | $this->addParameter('--all', type: $this::STRING); 926 | } 927 | 928 | protected function doExecute(): int|bool 929 | { 930 | $params = []; 931 | 932 | foreach ($this->params as $k => $v) { 933 | // Use any camel case convert library 934 | $params[Str::toCamelCase($k)] = $v; 935 | } 936 | 937 | return $this->{$this['task']}(...$params); 938 | } 939 | 940 | // `...$args` is required, otherwise the redundant params will make method calling error 941 | protected function build(string $libPath, string $tempPath, ...$args): int 942 | { 943 | $this->writeln("Building: $libPath | $tempPath"); 944 | return 0; 945 | } 946 | 947 | protected function clear(string $nested, string $dir, ...$args): int 948 | { 949 | $this->writeln("Clearing: $nested | $dir"); 950 | return 0; 951 | } 952 | }; 953 | $app->execute(); 954 | ``` 955 | 956 | Now run: 957 | 958 | ```bash 959 | php make.php build --lib-path foo --temp-path bar 960 | 961 | # Building foo | bar 962 | ``` 963 | 964 | 965 | ## Contributing and PR is Welcome 966 | 967 | I'm apologize that I'm too busy to fix or handle all issues and reports, but pull-request is very welcome 968 | and will speed up the fixing process. 969 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 2.0.3 2 | -------------------------------------------------------------------------------- /bin/release.php: -------------------------------------------------------------------------------- 1 | get('dry-run'); 19 | } 20 | } 21 | 22 | protected function configure(): void 23 | { 24 | $this->addParameter('version', type: static::STRING) 25 | ->description('Can be name or major|minor|patch|alpha|beta|rc') 26 | ->default('patch'); 27 | 28 | $this->addParameter('suffix', type: static::STRING) 29 | ->description('The suffix type. Can be alpha|beta|rc'); 30 | 31 | $this->addParameter('--dry-run|-d', type: static::BOOLEAN) 32 | ->description('Run process but do not execute any commands.'); 33 | 34 | $this->addParameter('--from', type: static::STRING) 35 | ->description('The version to release from. Default is the current version.') 36 | ->required(true); 37 | } 38 | 39 | protected function doExecute(): int 40 | { 41 | foreach ($this->scripts as $script) { 42 | $this->exec($script); 43 | } 44 | 45 | $currentVersion = $this->get('from') ?: trim(file_get_contents(__DIR__ . '/../VERSION')); 46 | $targetVersion = (string) $this->get('version'); 47 | $targetSuffix = (string) $this->get('suffix'); 48 | 49 | if (in_array($targetVersion, ['alpha', 'beta', 'rc'])) { 50 | $targetSuffix = $targetVersion; 51 | $targetVersion = 'patch'; 52 | } 53 | 54 | $targetVersion = static::versionPush($currentVersion, $targetVersion, $targetSuffix); 55 | 56 | $this->writeln('Release version: ' . $targetVersion); 57 | 58 | if (!$this->isDryRun) { 59 | static::writeVersion($targetVersion); 60 | } 61 | 62 | $this->exec(sprintf('git commit -am "Release version: %s"', $targetVersion)); 63 | $this->exec(sprintf('git tag %s', $targetVersion)); 64 | 65 | $this->exec('git push'); 66 | $this->exec('git push --tags'); 67 | 68 | return static::SUCCESS; 69 | } 70 | 71 | protected static function writeVersion(string $version): false|int 72 | { 73 | return file_put_contents(static::versionFile(), $version . "\n"); 74 | } 75 | 76 | protected static function versionFile(): string 77 | { 78 | return __DIR__ . '/../VERSION'; 79 | } 80 | 81 | protected static function versionPush( 82 | string $currentVersion, 83 | string $targetVersion, 84 | string $targetSuffix, 85 | ): string { 86 | [$major, $minor, $patch, $suffixType, $suffixVersion] = static::parseVersion($currentVersion); 87 | 88 | switch ($targetVersion) { 89 | case 'major': 90 | $major++; 91 | $minor = $patch = 0; 92 | if ($targetSuffix) { 93 | $suffixType = $targetSuffix; 94 | $suffixVersion = 1; 95 | } else { 96 | $suffixType = ''; 97 | $suffixVersion = 0; 98 | } 99 | break; 100 | 101 | case 'minor': 102 | $minor++; 103 | $patch = 0; 104 | if ($targetSuffix) { 105 | $suffixType = $targetSuffix; 106 | $suffixVersion = 1; 107 | } else { 108 | $suffixType = ''; 109 | $suffixVersion = 0; 110 | } 111 | break; 112 | 113 | case 'patch': 114 | if (!$suffixType) { 115 | $patch++; 116 | } 117 | if ($targetSuffix) { 118 | if ($suffixType === $targetSuffix) { 119 | $suffixVersion++; 120 | } else { 121 | $suffixType = $targetSuffix; 122 | $suffixVersion = 1; 123 | } 124 | } else { 125 | $suffixType = ''; 126 | $suffixVersion = 0; 127 | } 128 | break; 129 | 130 | default: 131 | return $targetVersion; 132 | } 133 | 134 | $currentVersion = $major . '.' . $minor . '.' . $patch; 135 | 136 | if ($suffixType) { 137 | $currentVersion .= '-' . $suffixType . '.' . $suffixVersion; 138 | } 139 | 140 | return $currentVersion; 141 | } 142 | 143 | public static function parseVersion(string $currentVersion): array 144 | { 145 | [$currentVersion, $prerelease] = explode('-', $currentVersion, 2) + ['', '']; 146 | 147 | [$major, $minor, $patch] = explode('.', $currentVersion, 3) + ['', '0', '0']; 148 | $major = (int) $major; 149 | $minor = (int) $minor; 150 | $patch = (int) $patch; 151 | $prereleaseType = ''; 152 | $prereleaseVersion = 0; 153 | 154 | if ($prerelease) { 155 | $matched = preg_match('/(rc|beta|alpha)[.-]?(\d+)/i', $prerelease, $matches); 156 | 157 | if ($matched) { 158 | $prereleaseType = strtolower($matches[1]); 159 | $prereleaseVersion = (int) $matches[2]; 160 | } 161 | } 162 | 163 | return [$major, $minor, $patch, $prereleaseType, $prereleaseVersion]; 164 | } 165 | 166 | public function exec(string $cmd, \Closure|null|false $output = null, bool $showCmd = true): ExecResult 167 | { 168 | $this->writeln('>> ' . ($this->isDryRun ? '(Dry Run) ' : '') . $cmd); 169 | 170 | if (!$this->isDryRun) { 171 | return parent::exec($cmd, $output, false); 172 | } 173 | 174 | return new ExecResult(); 175 | } 176 | 177 | public function addScript(string $script): static 178 | { 179 | $this->scripts[] = $script; 180 | 181 | return $this; 182 | } 183 | }; 184 | 185 | $app->execute(); 186 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "asika/simple-console", 3 | "description": "One file console framework to help you write build scripts.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Simon Asika", 8 | "email": "asika32764@gmail.com" 9 | } 10 | ], 11 | "minimum-stability": "beta", 12 | "require": { 13 | "php": ">=8.4" 14 | }, 15 | "autoload": { 16 | "files": [ 17 | "src/Console.php" 18 | ] 19 | }, 20 | "autoload-dev": { 21 | "psr-4": { 22 | "Asika\\SimpleConsole\\Test\\": "test/" 23 | } 24 | }, 25 | "require-dev": { 26 | "symfony/console": "^7.3@beta", 27 | "symfony/process": "^7.3@beta", 28 | "phpunit/phpunit": "^12.1", 29 | "symfony/var-dumper": "^7.3@beta", 30 | "windwalker/test": "^4.1", 31 | "windwalker/utilities": "^4.1" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "a82774cbcc975041c06bee47947d8457", 8 | "packages": [ 9 | { 10 | "name": "windwalker/test", 11 | "version": "4.1.7", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/windwalker-io/test.git", 15 | "reference": "36c6f8c361e21e6aa542bda4c164847937ad8682" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/windwalker-io/test/zipball/36c6f8c361e21e6aa542bda4c164847937ad8682", 20 | "reference": "36c6f8c361e21e6aa542bda4c164847937ad8682", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=8.2.0" 25 | }, 26 | "require-dev": { 27 | "phpunit/phpunit": "^8.0||^9.0||^10.0" 28 | }, 29 | "type": "windwalker-package", 30 | "extra": { 31 | "branch-alias": { 32 | "dev-master": "4.x-dev" 33 | } 34 | }, 35 | "autoload": { 36 | "files": [ 37 | "src/bootstrap.php" 38 | ], 39 | "psr-4": { 40 | "Windwalker\\Test\\": "src/" 41 | } 42 | }, 43 | "notification-url": "https://packagist.org/downloads/", 44 | "license": [ 45 | "MIT" 46 | ], 47 | "description": "Windwalker Test package", 48 | "homepage": "https://github.com/windwalker-io/test", 49 | "keywords": [ 50 | "framework", 51 | "test", 52 | "windwalker" 53 | ], 54 | "support": { 55 | "source": "https://github.com/windwalker-io/test/tree/4.1.7" 56 | }, 57 | "time": "2025-03-15T11:30:00+00:00" 58 | }, 59 | { 60 | "name": "windwalker/utilities", 61 | "version": "4.1.7", 62 | "source": { 63 | "type": "git", 64 | "url": "https://github.com/windwalker-io/utilities.git", 65 | "reference": "195c896f65a2e2d1995d1adef94ce6a2282cfeec" 66 | }, 67 | "dist": { 68 | "type": "zip", 69 | "url": "https://api.github.com/repos/windwalker-io/utilities/zipball/195c896f65a2e2d1995d1adef94ce6a2282cfeec", 70 | "reference": "195c896f65a2e2d1995d1adef94ce6a2282cfeec", 71 | "shasum": "" 72 | }, 73 | "require": { 74 | "php": ">=8.2.0" 75 | }, 76 | "require-dev": { 77 | "myclabs/php-enum": "^1.8", 78 | "phpunit/phpunit": "^8.0||^9.0||^10.0", 79 | "symfony/options-resolver": "^5.0||^6.0||^7.0", 80 | "symfony/string": "^5.2||^6.0||^7.0", 81 | "windwalker/attributes": "^4.0", 82 | "windwalker/scalars": "^4.0", 83 | "windwalker/test": "^4.0" 84 | }, 85 | "suggest": { 86 | "laravel/serializable-closure": "Install ^1.0 to support serializable Closure.", 87 | "symfony/string": "Install to use inflector." 88 | }, 89 | "type": "windwalker-package", 90 | "extra": { 91 | "branch-alias": { 92 | "dev-master": "4.x-dev" 93 | } 94 | }, 95 | "autoload": { 96 | "files": [ 97 | "src/bootstrap.php" 98 | ], 99 | "psr-4": { 100 | "Windwalker\\Utilities\\": "src/" 101 | } 102 | }, 103 | "notification-url": "https://packagist.org/downloads/", 104 | "license": [ 105 | "MIT" 106 | ], 107 | "description": "Windwalker Utilities package", 108 | "homepage": "https://github.com/windwalker-io/utilities", 109 | "keywords": [ 110 | "framework", 111 | "utilities", 112 | "windwalker" 113 | ], 114 | "support": { 115 | "source": "https://github.com/windwalker-io/utilities/tree/4.1.7" 116 | }, 117 | "time": "2025-03-14T19:53:33+00:00" 118 | } 119 | ], 120 | "packages-dev": [ 121 | { 122 | "name": "myclabs/deep-copy", 123 | "version": "1.13.1", 124 | "source": { 125 | "type": "git", 126 | "url": "https://github.com/myclabs/DeepCopy.git", 127 | "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" 128 | }, 129 | "dist": { 130 | "type": "zip", 131 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", 132 | "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", 133 | "shasum": "" 134 | }, 135 | "require": { 136 | "php": "^7.1 || ^8.0" 137 | }, 138 | "conflict": { 139 | "doctrine/collections": "<1.6.8", 140 | "doctrine/common": "<2.13.3 || >=3 <3.2.2" 141 | }, 142 | "require-dev": { 143 | "doctrine/collections": "^1.6.8", 144 | "doctrine/common": "^2.13.3 || ^3.2.2", 145 | "phpspec/prophecy": "^1.10", 146 | "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" 147 | }, 148 | "type": "library", 149 | "autoload": { 150 | "files": [ 151 | "src/DeepCopy/deep_copy.php" 152 | ], 153 | "psr-4": { 154 | "DeepCopy\\": "src/DeepCopy/" 155 | } 156 | }, 157 | "notification-url": "https://packagist.org/downloads/", 158 | "license": [ 159 | "MIT" 160 | ], 161 | "description": "Create deep copies (clones) of your objects", 162 | "keywords": [ 163 | "clone", 164 | "copy", 165 | "duplicate", 166 | "object", 167 | "object graph" 168 | ], 169 | "support": { 170 | "issues": "https://github.com/myclabs/DeepCopy/issues", 171 | "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" 172 | }, 173 | "funding": [ 174 | { 175 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 176 | "type": "tidelift" 177 | } 178 | ], 179 | "time": "2025-04-29T12:36:36+00:00" 180 | }, 181 | { 182 | "name": "nikic/php-parser", 183 | "version": "v5.4.0", 184 | "source": { 185 | "type": "git", 186 | "url": "https://github.com/nikic/PHP-Parser.git", 187 | "reference": "447a020a1f875a434d62f2a401f53b82a396e494" 188 | }, 189 | "dist": { 190 | "type": "zip", 191 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/447a020a1f875a434d62f2a401f53b82a396e494", 192 | "reference": "447a020a1f875a434d62f2a401f53b82a396e494", 193 | "shasum": "" 194 | }, 195 | "require": { 196 | "ext-ctype": "*", 197 | "ext-json": "*", 198 | "ext-tokenizer": "*", 199 | "php": ">=7.4" 200 | }, 201 | "require-dev": { 202 | "ircmaxell/php-yacc": "^0.0.7", 203 | "phpunit/phpunit": "^9.0" 204 | }, 205 | "bin": [ 206 | "bin/php-parse" 207 | ], 208 | "type": "library", 209 | "extra": { 210 | "branch-alias": { 211 | "dev-master": "5.0-dev" 212 | } 213 | }, 214 | "autoload": { 215 | "psr-4": { 216 | "PhpParser\\": "lib/PhpParser" 217 | } 218 | }, 219 | "notification-url": "https://packagist.org/downloads/", 220 | "license": [ 221 | "BSD-3-Clause" 222 | ], 223 | "authors": [ 224 | { 225 | "name": "Nikita Popov" 226 | } 227 | ], 228 | "description": "A PHP parser written in PHP", 229 | "keywords": [ 230 | "parser", 231 | "php" 232 | ], 233 | "support": { 234 | "issues": "https://github.com/nikic/PHP-Parser/issues", 235 | "source": "https://github.com/nikic/PHP-Parser/tree/v5.4.0" 236 | }, 237 | "time": "2024-12-30T11:07:19+00:00" 238 | }, 239 | { 240 | "name": "phar-io/manifest", 241 | "version": "2.0.4", 242 | "source": { 243 | "type": "git", 244 | "url": "https://github.com/phar-io/manifest.git", 245 | "reference": "54750ef60c58e43759730615a392c31c80e23176" 246 | }, 247 | "dist": { 248 | "type": "zip", 249 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", 250 | "reference": "54750ef60c58e43759730615a392c31c80e23176", 251 | "shasum": "" 252 | }, 253 | "require": { 254 | "ext-dom": "*", 255 | "ext-libxml": "*", 256 | "ext-phar": "*", 257 | "ext-xmlwriter": "*", 258 | "phar-io/version": "^3.0.1", 259 | "php": "^7.2 || ^8.0" 260 | }, 261 | "type": "library", 262 | "extra": { 263 | "branch-alias": { 264 | "dev-master": "2.0.x-dev" 265 | } 266 | }, 267 | "autoload": { 268 | "classmap": [ 269 | "src/" 270 | ] 271 | }, 272 | "notification-url": "https://packagist.org/downloads/", 273 | "license": [ 274 | "BSD-3-Clause" 275 | ], 276 | "authors": [ 277 | { 278 | "name": "Arne Blankerts", 279 | "email": "arne@blankerts.de", 280 | "role": "Developer" 281 | }, 282 | { 283 | "name": "Sebastian Heuer", 284 | "email": "sebastian@phpeople.de", 285 | "role": "Developer" 286 | }, 287 | { 288 | "name": "Sebastian Bergmann", 289 | "email": "sebastian@phpunit.de", 290 | "role": "Developer" 291 | } 292 | ], 293 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 294 | "support": { 295 | "issues": "https://github.com/phar-io/manifest/issues", 296 | "source": "https://github.com/phar-io/manifest/tree/2.0.4" 297 | }, 298 | "funding": [ 299 | { 300 | "url": "https://github.com/theseer", 301 | "type": "github" 302 | } 303 | ], 304 | "time": "2024-03-03T12:33:53+00:00" 305 | }, 306 | { 307 | "name": "phar-io/version", 308 | "version": "3.2.1", 309 | "source": { 310 | "type": "git", 311 | "url": "https://github.com/phar-io/version.git", 312 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" 313 | }, 314 | "dist": { 315 | "type": "zip", 316 | "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 317 | "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", 318 | "shasum": "" 319 | }, 320 | "require": { 321 | "php": "^7.2 || ^8.0" 322 | }, 323 | "type": "library", 324 | "autoload": { 325 | "classmap": [ 326 | "src/" 327 | ] 328 | }, 329 | "notification-url": "https://packagist.org/downloads/", 330 | "license": [ 331 | "BSD-3-Clause" 332 | ], 333 | "authors": [ 334 | { 335 | "name": "Arne Blankerts", 336 | "email": "arne@blankerts.de", 337 | "role": "Developer" 338 | }, 339 | { 340 | "name": "Sebastian Heuer", 341 | "email": "sebastian@phpeople.de", 342 | "role": "Developer" 343 | }, 344 | { 345 | "name": "Sebastian Bergmann", 346 | "email": "sebastian@phpunit.de", 347 | "role": "Developer" 348 | } 349 | ], 350 | "description": "Library for handling version information and constraints", 351 | "support": { 352 | "issues": "https://github.com/phar-io/version/issues", 353 | "source": "https://github.com/phar-io/version/tree/3.2.1" 354 | }, 355 | "time": "2022-02-21T01:04:05+00:00" 356 | }, 357 | { 358 | "name": "phpunit/php-code-coverage", 359 | "version": "12.2.1", 360 | "source": { 361 | "type": "git", 362 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 363 | "reference": "448f2c504d86dbff3949dcd02c95aa85db2c7617" 364 | }, 365 | "dist": { 366 | "type": "zip", 367 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/448f2c504d86dbff3949dcd02c95aa85db2c7617", 368 | "reference": "448f2c504d86dbff3949dcd02c95aa85db2c7617", 369 | "shasum": "" 370 | }, 371 | "require": { 372 | "ext-dom": "*", 373 | "ext-libxml": "*", 374 | "ext-xmlwriter": "*", 375 | "nikic/php-parser": "^5.4.0", 376 | "php": ">=8.3", 377 | "phpunit/php-file-iterator": "^6.0", 378 | "phpunit/php-text-template": "^5.0", 379 | "sebastian/complexity": "^5.0", 380 | "sebastian/environment": "^8.0", 381 | "sebastian/lines-of-code": "^4.0", 382 | "sebastian/version": "^6.0", 383 | "theseer/tokenizer": "^1.2.3" 384 | }, 385 | "require-dev": { 386 | "phpunit/phpunit": "^12.1" 387 | }, 388 | "suggest": { 389 | "ext-pcov": "PHP extension that provides line coverage", 390 | "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" 391 | }, 392 | "type": "library", 393 | "extra": { 394 | "branch-alias": { 395 | "dev-main": "12.2.x-dev" 396 | } 397 | }, 398 | "autoload": { 399 | "classmap": [ 400 | "src/" 401 | ] 402 | }, 403 | "notification-url": "https://packagist.org/downloads/", 404 | "license": [ 405 | "BSD-3-Clause" 406 | ], 407 | "authors": [ 408 | { 409 | "name": "Sebastian Bergmann", 410 | "email": "sebastian@phpunit.de", 411 | "role": "lead" 412 | } 413 | ], 414 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 415 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 416 | "keywords": [ 417 | "coverage", 418 | "testing", 419 | "xunit" 420 | ], 421 | "support": { 422 | "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", 423 | "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", 424 | "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.2.1" 425 | }, 426 | "funding": [ 427 | { 428 | "url": "https://github.com/sebastianbergmann", 429 | "type": "github" 430 | }, 431 | { 432 | "url": "https://liberapay.com/sebastianbergmann", 433 | "type": "liberapay" 434 | }, 435 | { 436 | "url": "https://thanks.dev/u/gh/sebastianbergmann", 437 | "type": "thanks_dev" 438 | }, 439 | { 440 | "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", 441 | "type": "tidelift" 442 | } 443 | ], 444 | "time": "2025-05-04T05:25:05+00:00" 445 | }, 446 | { 447 | "name": "phpunit/php-file-iterator", 448 | "version": "6.0.0", 449 | "source": { 450 | "type": "git", 451 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 452 | "reference": "961bc913d42fe24a257bfff826a5068079ac7782" 453 | }, 454 | "dist": { 455 | "type": "zip", 456 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/961bc913d42fe24a257bfff826a5068079ac7782", 457 | "reference": "961bc913d42fe24a257bfff826a5068079ac7782", 458 | "shasum": "" 459 | }, 460 | "require": { 461 | "php": ">=8.3" 462 | }, 463 | "require-dev": { 464 | "phpunit/phpunit": "^12.0" 465 | }, 466 | "type": "library", 467 | "extra": { 468 | "branch-alias": { 469 | "dev-main": "6.0-dev" 470 | } 471 | }, 472 | "autoload": { 473 | "classmap": [ 474 | "src/" 475 | ] 476 | }, 477 | "notification-url": "https://packagist.org/downloads/", 478 | "license": [ 479 | "BSD-3-Clause" 480 | ], 481 | "authors": [ 482 | { 483 | "name": "Sebastian Bergmann", 484 | "email": "sebastian@phpunit.de", 485 | "role": "lead" 486 | } 487 | ], 488 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 489 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 490 | "keywords": [ 491 | "filesystem", 492 | "iterator" 493 | ], 494 | "support": { 495 | "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", 496 | "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", 497 | "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.0" 498 | }, 499 | "funding": [ 500 | { 501 | "url": "https://github.com/sebastianbergmann", 502 | "type": "github" 503 | } 504 | ], 505 | "time": "2025-02-07T04:58:37+00:00" 506 | }, 507 | { 508 | "name": "phpunit/php-invoker", 509 | "version": "6.0.0", 510 | "source": { 511 | "type": "git", 512 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 513 | "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" 514 | }, 515 | "dist": { 516 | "type": "zip", 517 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", 518 | "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", 519 | "shasum": "" 520 | }, 521 | "require": { 522 | "php": ">=8.3" 523 | }, 524 | "require-dev": { 525 | "ext-pcntl": "*", 526 | "phpunit/phpunit": "^12.0" 527 | }, 528 | "suggest": { 529 | "ext-pcntl": "*" 530 | }, 531 | "type": "library", 532 | "extra": { 533 | "branch-alias": { 534 | "dev-main": "6.0-dev" 535 | } 536 | }, 537 | "autoload": { 538 | "classmap": [ 539 | "src/" 540 | ] 541 | }, 542 | "notification-url": "https://packagist.org/downloads/", 543 | "license": [ 544 | "BSD-3-Clause" 545 | ], 546 | "authors": [ 547 | { 548 | "name": "Sebastian Bergmann", 549 | "email": "sebastian@phpunit.de", 550 | "role": "lead" 551 | } 552 | ], 553 | "description": "Invoke callables with a timeout", 554 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 555 | "keywords": [ 556 | "process" 557 | ], 558 | "support": { 559 | "issues": "https://github.com/sebastianbergmann/php-invoker/issues", 560 | "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", 561 | "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" 562 | }, 563 | "funding": [ 564 | { 565 | "url": "https://github.com/sebastianbergmann", 566 | "type": "github" 567 | } 568 | ], 569 | "time": "2025-02-07T04:58:58+00:00" 570 | }, 571 | { 572 | "name": "phpunit/php-text-template", 573 | "version": "5.0.0", 574 | "source": { 575 | "type": "git", 576 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 577 | "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" 578 | }, 579 | "dist": { 580 | "type": "zip", 581 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", 582 | "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", 583 | "shasum": "" 584 | }, 585 | "require": { 586 | "php": ">=8.3" 587 | }, 588 | "require-dev": { 589 | "phpunit/phpunit": "^12.0" 590 | }, 591 | "type": "library", 592 | "extra": { 593 | "branch-alias": { 594 | "dev-main": "5.0-dev" 595 | } 596 | }, 597 | "autoload": { 598 | "classmap": [ 599 | "src/" 600 | ] 601 | }, 602 | "notification-url": "https://packagist.org/downloads/", 603 | "license": [ 604 | "BSD-3-Clause" 605 | ], 606 | "authors": [ 607 | { 608 | "name": "Sebastian Bergmann", 609 | "email": "sebastian@phpunit.de", 610 | "role": "lead" 611 | } 612 | ], 613 | "description": "Simple template engine.", 614 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 615 | "keywords": [ 616 | "template" 617 | ], 618 | "support": { 619 | "issues": "https://github.com/sebastianbergmann/php-text-template/issues", 620 | "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", 621 | "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" 622 | }, 623 | "funding": [ 624 | { 625 | "url": "https://github.com/sebastianbergmann", 626 | "type": "github" 627 | } 628 | ], 629 | "time": "2025-02-07T04:59:16+00:00" 630 | }, 631 | { 632 | "name": "phpunit/php-timer", 633 | "version": "8.0.0", 634 | "source": { 635 | "type": "git", 636 | "url": "https://github.com/sebastianbergmann/php-timer.git", 637 | "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" 638 | }, 639 | "dist": { 640 | "type": "zip", 641 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", 642 | "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", 643 | "shasum": "" 644 | }, 645 | "require": { 646 | "php": ">=8.3" 647 | }, 648 | "require-dev": { 649 | "phpunit/phpunit": "^12.0" 650 | }, 651 | "type": "library", 652 | "extra": { 653 | "branch-alias": { 654 | "dev-main": "8.0-dev" 655 | } 656 | }, 657 | "autoload": { 658 | "classmap": [ 659 | "src/" 660 | ] 661 | }, 662 | "notification-url": "https://packagist.org/downloads/", 663 | "license": [ 664 | "BSD-3-Clause" 665 | ], 666 | "authors": [ 667 | { 668 | "name": "Sebastian Bergmann", 669 | "email": "sebastian@phpunit.de", 670 | "role": "lead" 671 | } 672 | ], 673 | "description": "Utility class for timing", 674 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 675 | "keywords": [ 676 | "timer" 677 | ], 678 | "support": { 679 | "issues": "https://github.com/sebastianbergmann/php-timer/issues", 680 | "security": "https://github.com/sebastianbergmann/php-timer/security/policy", 681 | "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" 682 | }, 683 | "funding": [ 684 | { 685 | "url": "https://github.com/sebastianbergmann", 686 | "type": "github" 687 | } 688 | ], 689 | "time": "2025-02-07T04:59:38+00:00" 690 | }, 691 | { 692 | "name": "phpunit/phpunit", 693 | "version": "12.1.5", 694 | "source": { 695 | "type": "git", 696 | "url": "https://github.com/sebastianbergmann/phpunit.git", 697 | "reference": "f93ef2198df8d54b3195bcee381a33be51d8705e" 698 | }, 699 | "dist": { 700 | "type": "zip", 701 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f93ef2198df8d54b3195bcee381a33be51d8705e", 702 | "reference": "f93ef2198df8d54b3195bcee381a33be51d8705e", 703 | "shasum": "" 704 | }, 705 | "require": { 706 | "ext-dom": "*", 707 | "ext-json": "*", 708 | "ext-libxml": "*", 709 | "ext-mbstring": "*", 710 | "ext-xml": "*", 711 | "ext-xmlwriter": "*", 712 | "myclabs/deep-copy": "^1.13.1", 713 | "phar-io/manifest": "^2.0.4", 714 | "phar-io/version": "^3.2.1", 715 | "php": ">=8.3", 716 | "phpunit/php-code-coverage": "^12.2.1", 717 | "phpunit/php-file-iterator": "^6.0.0", 718 | "phpunit/php-invoker": "^6.0.0", 719 | "phpunit/php-text-template": "^5.0.0", 720 | "phpunit/php-timer": "^8.0.0", 721 | "sebastian/cli-parser": "^4.0.0", 722 | "sebastian/comparator": "^7.0.1", 723 | "sebastian/diff": "^7.0.0", 724 | "sebastian/environment": "^8.0.0", 725 | "sebastian/exporter": "^7.0.0", 726 | "sebastian/global-state": "^8.0.0", 727 | "sebastian/object-enumerator": "^7.0.0", 728 | "sebastian/type": "^6.0.2", 729 | "sebastian/version": "^6.0.0", 730 | "staabm/side-effects-detector": "^1.0.5" 731 | }, 732 | "bin": [ 733 | "phpunit" 734 | ], 735 | "type": "library", 736 | "extra": { 737 | "branch-alias": { 738 | "dev-main": "12.1-dev" 739 | } 740 | }, 741 | "autoload": { 742 | "files": [ 743 | "src/Framework/Assert/Functions.php" 744 | ], 745 | "classmap": [ 746 | "src/" 747 | ] 748 | }, 749 | "notification-url": "https://packagist.org/downloads/", 750 | "license": [ 751 | "BSD-3-Clause" 752 | ], 753 | "authors": [ 754 | { 755 | "name": "Sebastian Bergmann", 756 | "email": "sebastian@phpunit.de", 757 | "role": "lead" 758 | } 759 | ], 760 | "description": "The PHP Unit Testing framework.", 761 | "homepage": "https://phpunit.de/", 762 | "keywords": [ 763 | "phpunit", 764 | "testing", 765 | "xunit" 766 | ], 767 | "support": { 768 | "issues": "https://github.com/sebastianbergmann/phpunit/issues", 769 | "security": "https://github.com/sebastianbergmann/phpunit/security/policy", 770 | "source": "https://github.com/sebastianbergmann/phpunit/tree/12.1.5" 771 | }, 772 | "funding": [ 773 | { 774 | "url": "https://phpunit.de/sponsors.html", 775 | "type": "custom" 776 | }, 777 | { 778 | "url": "https://github.com/sebastianbergmann", 779 | "type": "github" 780 | }, 781 | { 782 | "url": "https://liberapay.com/sebastianbergmann", 783 | "type": "liberapay" 784 | }, 785 | { 786 | "url": "https://thanks.dev/u/gh/sebastianbergmann", 787 | "type": "thanks_dev" 788 | }, 789 | { 790 | "url": "https://tidelift.com/funding/github/packagist/phpunit/phpunit", 791 | "type": "tidelift" 792 | } 793 | ], 794 | "time": "2025-05-11T06:44:52+00:00" 795 | }, 796 | { 797 | "name": "psr/container", 798 | "version": "2.0.2", 799 | "source": { 800 | "type": "git", 801 | "url": "https://github.com/php-fig/container.git", 802 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963" 803 | }, 804 | "dist": { 805 | "type": "zip", 806 | "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963", 807 | "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963", 808 | "shasum": "" 809 | }, 810 | "require": { 811 | "php": ">=7.4.0" 812 | }, 813 | "type": "library", 814 | "extra": { 815 | "branch-alias": { 816 | "dev-master": "2.0.x-dev" 817 | } 818 | }, 819 | "autoload": { 820 | "psr-4": { 821 | "Psr\\Container\\": "src/" 822 | } 823 | }, 824 | "notification-url": "https://packagist.org/downloads/", 825 | "license": [ 826 | "MIT" 827 | ], 828 | "authors": [ 829 | { 830 | "name": "PHP-FIG", 831 | "homepage": "https://www.php-fig.org/" 832 | } 833 | ], 834 | "description": "Common Container Interface (PHP FIG PSR-11)", 835 | "homepage": "https://github.com/php-fig/container", 836 | "keywords": [ 837 | "PSR-11", 838 | "container", 839 | "container-interface", 840 | "container-interop", 841 | "psr" 842 | ], 843 | "support": { 844 | "issues": "https://github.com/php-fig/container/issues", 845 | "source": "https://github.com/php-fig/container/tree/2.0.2" 846 | }, 847 | "time": "2021-11-05T16:47:00+00:00" 848 | }, 849 | { 850 | "name": "sebastian/cli-parser", 851 | "version": "4.0.0", 852 | "source": { 853 | "type": "git", 854 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 855 | "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c" 856 | }, 857 | "dist": { 858 | "type": "zip", 859 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/6d584c727d9114bcdc14c86711cd1cad51778e7c", 860 | "reference": "6d584c727d9114bcdc14c86711cd1cad51778e7c", 861 | "shasum": "" 862 | }, 863 | "require": { 864 | "php": ">=8.3" 865 | }, 866 | "require-dev": { 867 | "phpunit/phpunit": "^12.0" 868 | }, 869 | "type": "library", 870 | "extra": { 871 | "branch-alias": { 872 | "dev-main": "4.0-dev" 873 | } 874 | }, 875 | "autoload": { 876 | "classmap": [ 877 | "src/" 878 | ] 879 | }, 880 | "notification-url": "https://packagist.org/downloads/", 881 | "license": [ 882 | "BSD-3-Clause" 883 | ], 884 | "authors": [ 885 | { 886 | "name": "Sebastian Bergmann", 887 | "email": "sebastian@phpunit.de", 888 | "role": "lead" 889 | } 890 | ], 891 | "description": "Library for parsing CLI options", 892 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 893 | "support": { 894 | "issues": "https://github.com/sebastianbergmann/cli-parser/issues", 895 | "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", 896 | "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.0.0" 897 | }, 898 | "funding": [ 899 | { 900 | "url": "https://github.com/sebastianbergmann", 901 | "type": "github" 902 | } 903 | ], 904 | "time": "2025-02-07T04:53:50+00:00" 905 | }, 906 | { 907 | "name": "sebastian/comparator", 908 | "version": "7.0.1", 909 | "source": { 910 | "type": "git", 911 | "url": "https://github.com/sebastianbergmann/comparator.git", 912 | "reference": "b478f34614f934e0291598d0c08cbaba9644bee5" 913 | }, 914 | "dist": { 915 | "type": "zip", 916 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/b478f34614f934e0291598d0c08cbaba9644bee5", 917 | "reference": "b478f34614f934e0291598d0c08cbaba9644bee5", 918 | "shasum": "" 919 | }, 920 | "require": { 921 | "ext-dom": "*", 922 | "ext-mbstring": "*", 923 | "php": ">=8.3", 924 | "sebastian/diff": "^7.0", 925 | "sebastian/exporter": "^7.0" 926 | }, 927 | "require-dev": { 928 | "phpunit/phpunit": "^12.0" 929 | }, 930 | "suggest": { 931 | "ext-bcmath": "For comparing BcMath\\Number objects" 932 | }, 933 | "type": "library", 934 | "extra": { 935 | "branch-alias": { 936 | "dev-main": "7.0-dev" 937 | } 938 | }, 939 | "autoload": { 940 | "classmap": [ 941 | "src/" 942 | ] 943 | }, 944 | "notification-url": "https://packagist.org/downloads/", 945 | "license": [ 946 | "BSD-3-Clause" 947 | ], 948 | "authors": [ 949 | { 950 | "name": "Sebastian Bergmann", 951 | "email": "sebastian@phpunit.de" 952 | }, 953 | { 954 | "name": "Jeff Welch", 955 | "email": "whatthejeff@gmail.com" 956 | }, 957 | { 958 | "name": "Volker Dusch", 959 | "email": "github@wallbash.com" 960 | }, 961 | { 962 | "name": "Bernhard Schussek", 963 | "email": "bschussek@2bepublished.at" 964 | } 965 | ], 966 | "description": "Provides the functionality to compare PHP values for equality", 967 | "homepage": "https://github.com/sebastianbergmann/comparator", 968 | "keywords": [ 969 | "comparator", 970 | "compare", 971 | "equality" 972 | ], 973 | "support": { 974 | "issues": "https://github.com/sebastianbergmann/comparator/issues", 975 | "security": "https://github.com/sebastianbergmann/comparator/security/policy", 976 | "source": "https://github.com/sebastianbergmann/comparator/tree/7.0.1" 977 | }, 978 | "funding": [ 979 | { 980 | "url": "https://github.com/sebastianbergmann", 981 | "type": "github" 982 | } 983 | ], 984 | "time": "2025-03-07T07:00:32+00:00" 985 | }, 986 | { 987 | "name": "sebastian/complexity", 988 | "version": "5.0.0", 989 | "source": { 990 | "type": "git", 991 | "url": "https://github.com/sebastianbergmann/complexity.git", 992 | "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" 993 | }, 994 | "dist": { 995 | "type": "zip", 996 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", 997 | "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", 998 | "shasum": "" 999 | }, 1000 | "require": { 1001 | "nikic/php-parser": "^5.0", 1002 | "php": ">=8.3" 1003 | }, 1004 | "require-dev": { 1005 | "phpunit/phpunit": "^12.0" 1006 | }, 1007 | "type": "library", 1008 | "extra": { 1009 | "branch-alias": { 1010 | "dev-main": "5.0-dev" 1011 | } 1012 | }, 1013 | "autoload": { 1014 | "classmap": [ 1015 | "src/" 1016 | ] 1017 | }, 1018 | "notification-url": "https://packagist.org/downloads/", 1019 | "license": [ 1020 | "BSD-3-Clause" 1021 | ], 1022 | "authors": [ 1023 | { 1024 | "name": "Sebastian Bergmann", 1025 | "email": "sebastian@phpunit.de", 1026 | "role": "lead" 1027 | } 1028 | ], 1029 | "description": "Library for calculating the complexity of PHP code units", 1030 | "homepage": "https://github.com/sebastianbergmann/complexity", 1031 | "support": { 1032 | "issues": "https://github.com/sebastianbergmann/complexity/issues", 1033 | "security": "https://github.com/sebastianbergmann/complexity/security/policy", 1034 | "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" 1035 | }, 1036 | "funding": [ 1037 | { 1038 | "url": "https://github.com/sebastianbergmann", 1039 | "type": "github" 1040 | } 1041 | ], 1042 | "time": "2025-02-07T04:55:25+00:00" 1043 | }, 1044 | { 1045 | "name": "sebastian/diff", 1046 | "version": "7.0.0", 1047 | "source": { 1048 | "type": "git", 1049 | "url": "https://github.com/sebastianbergmann/diff.git", 1050 | "reference": "7ab1ea946c012266ca32390913653d844ecd085f" 1051 | }, 1052 | "dist": { 1053 | "type": "zip", 1054 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", 1055 | "reference": "7ab1ea946c012266ca32390913653d844ecd085f", 1056 | "shasum": "" 1057 | }, 1058 | "require": { 1059 | "php": ">=8.3" 1060 | }, 1061 | "require-dev": { 1062 | "phpunit/phpunit": "^12.0", 1063 | "symfony/process": "^7.2" 1064 | }, 1065 | "type": "library", 1066 | "extra": { 1067 | "branch-alias": { 1068 | "dev-main": "7.0-dev" 1069 | } 1070 | }, 1071 | "autoload": { 1072 | "classmap": [ 1073 | "src/" 1074 | ] 1075 | }, 1076 | "notification-url": "https://packagist.org/downloads/", 1077 | "license": [ 1078 | "BSD-3-Clause" 1079 | ], 1080 | "authors": [ 1081 | { 1082 | "name": "Sebastian Bergmann", 1083 | "email": "sebastian@phpunit.de" 1084 | }, 1085 | { 1086 | "name": "Kore Nordmann", 1087 | "email": "mail@kore-nordmann.de" 1088 | } 1089 | ], 1090 | "description": "Diff implementation", 1091 | "homepage": "https://github.com/sebastianbergmann/diff", 1092 | "keywords": [ 1093 | "diff", 1094 | "udiff", 1095 | "unidiff", 1096 | "unified diff" 1097 | ], 1098 | "support": { 1099 | "issues": "https://github.com/sebastianbergmann/diff/issues", 1100 | "security": "https://github.com/sebastianbergmann/diff/security/policy", 1101 | "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" 1102 | }, 1103 | "funding": [ 1104 | { 1105 | "url": "https://github.com/sebastianbergmann", 1106 | "type": "github" 1107 | } 1108 | ], 1109 | "time": "2025-02-07T04:55:46+00:00" 1110 | }, 1111 | { 1112 | "name": "sebastian/environment", 1113 | "version": "8.0.0", 1114 | "source": { 1115 | "type": "git", 1116 | "url": "https://github.com/sebastianbergmann/environment.git", 1117 | "reference": "8afe311eca49171bf95405cc0078be9a3821f9f2" 1118 | }, 1119 | "dist": { 1120 | "type": "zip", 1121 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8afe311eca49171bf95405cc0078be9a3821f9f2", 1122 | "reference": "8afe311eca49171bf95405cc0078be9a3821f9f2", 1123 | "shasum": "" 1124 | }, 1125 | "require": { 1126 | "php": ">=8.3" 1127 | }, 1128 | "require-dev": { 1129 | "phpunit/phpunit": "^12.0" 1130 | }, 1131 | "suggest": { 1132 | "ext-posix": "*" 1133 | }, 1134 | "type": "library", 1135 | "extra": { 1136 | "branch-alias": { 1137 | "dev-main": "8.0-dev" 1138 | } 1139 | }, 1140 | "autoload": { 1141 | "classmap": [ 1142 | "src/" 1143 | ] 1144 | }, 1145 | "notification-url": "https://packagist.org/downloads/", 1146 | "license": [ 1147 | "BSD-3-Clause" 1148 | ], 1149 | "authors": [ 1150 | { 1151 | "name": "Sebastian Bergmann", 1152 | "email": "sebastian@phpunit.de" 1153 | } 1154 | ], 1155 | "description": "Provides functionality to handle HHVM/PHP environments", 1156 | "homepage": "https://github.com/sebastianbergmann/environment", 1157 | "keywords": [ 1158 | "Xdebug", 1159 | "environment", 1160 | "hhvm" 1161 | ], 1162 | "support": { 1163 | "issues": "https://github.com/sebastianbergmann/environment/issues", 1164 | "security": "https://github.com/sebastianbergmann/environment/security/policy", 1165 | "source": "https://github.com/sebastianbergmann/environment/tree/8.0.0" 1166 | }, 1167 | "funding": [ 1168 | { 1169 | "url": "https://github.com/sebastianbergmann", 1170 | "type": "github" 1171 | } 1172 | ], 1173 | "time": "2025-02-07T04:56:08+00:00" 1174 | }, 1175 | { 1176 | "name": "sebastian/exporter", 1177 | "version": "7.0.0", 1178 | "source": { 1179 | "type": "git", 1180 | "url": "https://github.com/sebastianbergmann/exporter.git", 1181 | "reference": "76432aafc58d50691a00d86d0632f1217a47b688" 1182 | }, 1183 | "dist": { 1184 | "type": "zip", 1185 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/76432aafc58d50691a00d86d0632f1217a47b688", 1186 | "reference": "76432aafc58d50691a00d86d0632f1217a47b688", 1187 | "shasum": "" 1188 | }, 1189 | "require": { 1190 | "ext-mbstring": "*", 1191 | "php": ">=8.3", 1192 | "sebastian/recursion-context": "^7.0" 1193 | }, 1194 | "require-dev": { 1195 | "phpunit/phpunit": "^12.0" 1196 | }, 1197 | "type": "library", 1198 | "extra": { 1199 | "branch-alias": { 1200 | "dev-main": "7.0-dev" 1201 | } 1202 | }, 1203 | "autoload": { 1204 | "classmap": [ 1205 | "src/" 1206 | ] 1207 | }, 1208 | "notification-url": "https://packagist.org/downloads/", 1209 | "license": [ 1210 | "BSD-3-Clause" 1211 | ], 1212 | "authors": [ 1213 | { 1214 | "name": "Sebastian Bergmann", 1215 | "email": "sebastian@phpunit.de" 1216 | }, 1217 | { 1218 | "name": "Jeff Welch", 1219 | "email": "whatthejeff@gmail.com" 1220 | }, 1221 | { 1222 | "name": "Volker Dusch", 1223 | "email": "github@wallbash.com" 1224 | }, 1225 | { 1226 | "name": "Adam Harvey", 1227 | "email": "aharvey@php.net" 1228 | }, 1229 | { 1230 | "name": "Bernhard Schussek", 1231 | "email": "bschussek@gmail.com" 1232 | } 1233 | ], 1234 | "description": "Provides the functionality to export PHP variables for visualization", 1235 | "homepage": "https://www.github.com/sebastianbergmann/exporter", 1236 | "keywords": [ 1237 | "export", 1238 | "exporter" 1239 | ], 1240 | "support": { 1241 | "issues": "https://github.com/sebastianbergmann/exporter/issues", 1242 | "security": "https://github.com/sebastianbergmann/exporter/security/policy", 1243 | "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.0" 1244 | }, 1245 | "funding": [ 1246 | { 1247 | "url": "https://github.com/sebastianbergmann", 1248 | "type": "github" 1249 | } 1250 | ], 1251 | "time": "2025-02-07T04:56:42+00:00" 1252 | }, 1253 | { 1254 | "name": "sebastian/global-state", 1255 | "version": "8.0.0", 1256 | "source": { 1257 | "type": "git", 1258 | "url": "https://github.com/sebastianbergmann/global-state.git", 1259 | "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc" 1260 | }, 1261 | "dist": { 1262 | "type": "zip", 1263 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/570a2aeb26d40f057af686d63c4e99b075fb6cbc", 1264 | "reference": "570a2aeb26d40f057af686d63c4e99b075fb6cbc", 1265 | "shasum": "" 1266 | }, 1267 | "require": { 1268 | "php": ">=8.3", 1269 | "sebastian/object-reflector": "^5.0", 1270 | "sebastian/recursion-context": "^7.0" 1271 | }, 1272 | "require-dev": { 1273 | "ext-dom": "*", 1274 | "phpunit/phpunit": "^12.0" 1275 | }, 1276 | "type": "library", 1277 | "extra": { 1278 | "branch-alias": { 1279 | "dev-main": "8.0-dev" 1280 | } 1281 | }, 1282 | "autoload": { 1283 | "classmap": [ 1284 | "src/" 1285 | ] 1286 | }, 1287 | "notification-url": "https://packagist.org/downloads/", 1288 | "license": [ 1289 | "BSD-3-Clause" 1290 | ], 1291 | "authors": [ 1292 | { 1293 | "name": "Sebastian Bergmann", 1294 | "email": "sebastian@phpunit.de" 1295 | } 1296 | ], 1297 | "description": "Snapshotting of global state", 1298 | "homepage": "https://www.github.com/sebastianbergmann/global-state", 1299 | "keywords": [ 1300 | "global state" 1301 | ], 1302 | "support": { 1303 | "issues": "https://github.com/sebastianbergmann/global-state/issues", 1304 | "security": "https://github.com/sebastianbergmann/global-state/security/policy", 1305 | "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.0" 1306 | }, 1307 | "funding": [ 1308 | { 1309 | "url": "https://github.com/sebastianbergmann", 1310 | "type": "github" 1311 | } 1312 | ], 1313 | "time": "2025-02-07T04:56:59+00:00" 1314 | }, 1315 | { 1316 | "name": "sebastian/lines-of-code", 1317 | "version": "4.0.0", 1318 | "source": { 1319 | "type": "git", 1320 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1321 | "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" 1322 | }, 1323 | "dist": { 1324 | "type": "zip", 1325 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", 1326 | "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", 1327 | "shasum": "" 1328 | }, 1329 | "require": { 1330 | "nikic/php-parser": "^5.0", 1331 | "php": ">=8.3" 1332 | }, 1333 | "require-dev": { 1334 | "phpunit/phpunit": "^12.0" 1335 | }, 1336 | "type": "library", 1337 | "extra": { 1338 | "branch-alias": { 1339 | "dev-main": "4.0-dev" 1340 | } 1341 | }, 1342 | "autoload": { 1343 | "classmap": [ 1344 | "src/" 1345 | ] 1346 | }, 1347 | "notification-url": "https://packagist.org/downloads/", 1348 | "license": [ 1349 | "BSD-3-Clause" 1350 | ], 1351 | "authors": [ 1352 | { 1353 | "name": "Sebastian Bergmann", 1354 | "email": "sebastian@phpunit.de", 1355 | "role": "lead" 1356 | } 1357 | ], 1358 | "description": "Library for counting the lines of code in PHP source code", 1359 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1360 | "support": { 1361 | "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", 1362 | "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", 1363 | "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" 1364 | }, 1365 | "funding": [ 1366 | { 1367 | "url": "https://github.com/sebastianbergmann", 1368 | "type": "github" 1369 | } 1370 | ], 1371 | "time": "2025-02-07T04:57:28+00:00" 1372 | }, 1373 | { 1374 | "name": "sebastian/object-enumerator", 1375 | "version": "7.0.0", 1376 | "source": { 1377 | "type": "git", 1378 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1379 | "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" 1380 | }, 1381 | "dist": { 1382 | "type": "zip", 1383 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", 1384 | "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", 1385 | "shasum": "" 1386 | }, 1387 | "require": { 1388 | "php": ">=8.3", 1389 | "sebastian/object-reflector": "^5.0", 1390 | "sebastian/recursion-context": "^7.0" 1391 | }, 1392 | "require-dev": { 1393 | "phpunit/phpunit": "^12.0" 1394 | }, 1395 | "type": "library", 1396 | "extra": { 1397 | "branch-alias": { 1398 | "dev-main": "7.0-dev" 1399 | } 1400 | }, 1401 | "autoload": { 1402 | "classmap": [ 1403 | "src/" 1404 | ] 1405 | }, 1406 | "notification-url": "https://packagist.org/downloads/", 1407 | "license": [ 1408 | "BSD-3-Clause" 1409 | ], 1410 | "authors": [ 1411 | { 1412 | "name": "Sebastian Bergmann", 1413 | "email": "sebastian@phpunit.de" 1414 | } 1415 | ], 1416 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1417 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1418 | "support": { 1419 | "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", 1420 | "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", 1421 | "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" 1422 | }, 1423 | "funding": [ 1424 | { 1425 | "url": "https://github.com/sebastianbergmann", 1426 | "type": "github" 1427 | } 1428 | ], 1429 | "time": "2025-02-07T04:57:48+00:00" 1430 | }, 1431 | { 1432 | "name": "sebastian/object-reflector", 1433 | "version": "5.0.0", 1434 | "source": { 1435 | "type": "git", 1436 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1437 | "reference": "4bfa827c969c98be1e527abd576533293c634f6a" 1438 | }, 1439 | "dist": { 1440 | "type": "zip", 1441 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", 1442 | "reference": "4bfa827c969c98be1e527abd576533293c634f6a", 1443 | "shasum": "" 1444 | }, 1445 | "require": { 1446 | "php": ">=8.3" 1447 | }, 1448 | "require-dev": { 1449 | "phpunit/phpunit": "^12.0" 1450 | }, 1451 | "type": "library", 1452 | "extra": { 1453 | "branch-alias": { 1454 | "dev-main": "5.0-dev" 1455 | } 1456 | }, 1457 | "autoload": { 1458 | "classmap": [ 1459 | "src/" 1460 | ] 1461 | }, 1462 | "notification-url": "https://packagist.org/downloads/", 1463 | "license": [ 1464 | "BSD-3-Clause" 1465 | ], 1466 | "authors": [ 1467 | { 1468 | "name": "Sebastian Bergmann", 1469 | "email": "sebastian@phpunit.de" 1470 | } 1471 | ], 1472 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1473 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1474 | "support": { 1475 | "issues": "https://github.com/sebastianbergmann/object-reflector/issues", 1476 | "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", 1477 | "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" 1478 | }, 1479 | "funding": [ 1480 | { 1481 | "url": "https://github.com/sebastianbergmann", 1482 | "type": "github" 1483 | } 1484 | ], 1485 | "time": "2025-02-07T04:58:17+00:00" 1486 | }, 1487 | { 1488 | "name": "sebastian/recursion-context", 1489 | "version": "7.0.0", 1490 | "source": { 1491 | "type": "git", 1492 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1493 | "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c" 1494 | }, 1495 | "dist": { 1496 | "type": "zip", 1497 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/c405ae3a63e01b32eb71577f8ec1604e39858a7c", 1498 | "reference": "c405ae3a63e01b32eb71577f8ec1604e39858a7c", 1499 | "shasum": "" 1500 | }, 1501 | "require": { 1502 | "php": ">=8.3" 1503 | }, 1504 | "require-dev": { 1505 | "phpunit/phpunit": "^12.0" 1506 | }, 1507 | "type": "library", 1508 | "extra": { 1509 | "branch-alias": { 1510 | "dev-main": "7.0-dev" 1511 | } 1512 | }, 1513 | "autoload": { 1514 | "classmap": [ 1515 | "src/" 1516 | ] 1517 | }, 1518 | "notification-url": "https://packagist.org/downloads/", 1519 | "license": [ 1520 | "BSD-3-Clause" 1521 | ], 1522 | "authors": [ 1523 | { 1524 | "name": "Sebastian Bergmann", 1525 | "email": "sebastian@phpunit.de" 1526 | }, 1527 | { 1528 | "name": "Jeff Welch", 1529 | "email": "whatthejeff@gmail.com" 1530 | }, 1531 | { 1532 | "name": "Adam Harvey", 1533 | "email": "aharvey@php.net" 1534 | } 1535 | ], 1536 | "description": "Provides functionality to recursively process PHP variables", 1537 | "homepage": "https://github.com/sebastianbergmann/recursion-context", 1538 | "support": { 1539 | "issues": "https://github.com/sebastianbergmann/recursion-context/issues", 1540 | "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", 1541 | "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.0" 1542 | }, 1543 | "funding": [ 1544 | { 1545 | "url": "https://github.com/sebastianbergmann", 1546 | "type": "github" 1547 | } 1548 | ], 1549 | "time": "2025-02-07T05:00:01+00:00" 1550 | }, 1551 | { 1552 | "name": "sebastian/type", 1553 | "version": "6.0.2", 1554 | "source": { 1555 | "type": "git", 1556 | "url": "https://github.com/sebastianbergmann/type.git", 1557 | "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069" 1558 | }, 1559 | "dist": { 1560 | "type": "zip", 1561 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/1d7cd6e514384c36d7a390347f57c385d4be6069", 1562 | "reference": "1d7cd6e514384c36d7a390347f57c385d4be6069", 1563 | "shasum": "" 1564 | }, 1565 | "require": { 1566 | "php": ">=8.3" 1567 | }, 1568 | "require-dev": { 1569 | "phpunit/phpunit": "^12.0" 1570 | }, 1571 | "type": "library", 1572 | "extra": { 1573 | "branch-alias": { 1574 | "dev-main": "6.0-dev" 1575 | } 1576 | }, 1577 | "autoload": { 1578 | "classmap": [ 1579 | "src/" 1580 | ] 1581 | }, 1582 | "notification-url": "https://packagist.org/downloads/", 1583 | "license": [ 1584 | "BSD-3-Clause" 1585 | ], 1586 | "authors": [ 1587 | { 1588 | "name": "Sebastian Bergmann", 1589 | "email": "sebastian@phpunit.de", 1590 | "role": "lead" 1591 | } 1592 | ], 1593 | "description": "Collection of value objects that represent the types of the PHP type system", 1594 | "homepage": "https://github.com/sebastianbergmann/type", 1595 | "support": { 1596 | "issues": "https://github.com/sebastianbergmann/type/issues", 1597 | "security": "https://github.com/sebastianbergmann/type/security/policy", 1598 | "source": "https://github.com/sebastianbergmann/type/tree/6.0.2" 1599 | }, 1600 | "funding": [ 1601 | { 1602 | "url": "https://github.com/sebastianbergmann", 1603 | "type": "github" 1604 | } 1605 | ], 1606 | "time": "2025-03-18T13:37:31+00:00" 1607 | }, 1608 | { 1609 | "name": "sebastian/version", 1610 | "version": "6.0.0", 1611 | "source": { 1612 | "type": "git", 1613 | "url": "https://github.com/sebastianbergmann/version.git", 1614 | "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" 1615 | }, 1616 | "dist": { 1617 | "type": "zip", 1618 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", 1619 | "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", 1620 | "shasum": "" 1621 | }, 1622 | "require": { 1623 | "php": ">=8.3" 1624 | }, 1625 | "type": "library", 1626 | "extra": { 1627 | "branch-alias": { 1628 | "dev-main": "6.0-dev" 1629 | } 1630 | }, 1631 | "autoload": { 1632 | "classmap": [ 1633 | "src/" 1634 | ] 1635 | }, 1636 | "notification-url": "https://packagist.org/downloads/", 1637 | "license": [ 1638 | "BSD-3-Clause" 1639 | ], 1640 | "authors": [ 1641 | { 1642 | "name": "Sebastian Bergmann", 1643 | "email": "sebastian@phpunit.de", 1644 | "role": "lead" 1645 | } 1646 | ], 1647 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1648 | "homepage": "https://github.com/sebastianbergmann/version", 1649 | "support": { 1650 | "issues": "https://github.com/sebastianbergmann/version/issues", 1651 | "security": "https://github.com/sebastianbergmann/version/security/policy", 1652 | "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" 1653 | }, 1654 | "funding": [ 1655 | { 1656 | "url": "https://github.com/sebastianbergmann", 1657 | "type": "github" 1658 | } 1659 | ], 1660 | "time": "2025-02-07T05:00:38+00:00" 1661 | }, 1662 | { 1663 | "name": "staabm/side-effects-detector", 1664 | "version": "1.0.5", 1665 | "source": { 1666 | "type": "git", 1667 | "url": "https://github.com/staabm/side-effects-detector.git", 1668 | "reference": "d8334211a140ce329c13726d4a715adbddd0a163" 1669 | }, 1670 | "dist": { 1671 | "type": "zip", 1672 | "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", 1673 | "reference": "d8334211a140ce329c13726d4a715adbddd0a163", 1674 | "shasum": "" 1675 | }, 1676 | "require": { 1677 | "ext-tokenizer": "*", 1678 | "php": "^7.4 || ^8.0" 1679 | }, 1680 | "require-dev": { 1681 | "phpstan/extension-installer": "^1.4.3", 1682 | "phpstan/phpstan": "^1.12.6", 1683 | "phpunit/phpunit": "^9.6.21", 1684 | "symfony/var-dumper": "^5.4.43", 1685 | "tomasvotruba/type-coverage": "1.0.0", 1686 | "tomasvotruba/unused-public": "1.0.0" 1687 | }, 1688 | "type": "library", 1689 | "autoload": { 1690 | "classmap": [ 1691 | "lib/" 1692 | ] 1693 | }, 1694 | "notification-url": "https://packagist.org/downloads/", 1695 | "license": [ 1696 | "MIT" 1697 | ], 1698 | "description": "A static analysis tool to detect side effects in PHP code", 1699 | "keywords": [ 1700 | "static analysis" 1701 | ], 1702 | "support": { 1703 | "issues": "https://github.com/staabm/side-effects-detector/issues", 1704 | "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" 1705 | }, 1706 | "funding": [ 1707 | { 1708 | "url": "https://github.com/staabm", 1709 | "type": "github" 1710 | } 1711 | ], 1712 | "time": "2024-10-20T05:08:20+00:00" 1713 | }, 1714 | { 1715 | "name": "symfony/console", 1716 | "version": "v7.3.0-BETA2", 1717 | "source": { 1718 | "type": "git", 1719 | "url": "https://github.com/symfony/console.git", 1720 | "reference": "aa8b412c06f682ba488b1d3589aa544cd1eff0c0" 1721 | }, 1722 | "dist": { 1723 | "type": "zip", 1724 | "url": "https://api.github.com/repos/symfony/console/zipball/aa8b412c06f682ba488b1d3589aa544cd1eff0c0", 1725 | "reference": "aa8b412c06f682ba488b1d3589aa544cd1eff0c0", 1726 | "shasum": "" 1727 | }, 1728 | "require": { 1729 | "php": ">=8.2", 1730 | "symfony/deprecation-contracts": "^2.5|^3", 1731 | "symfony/polyfill-mbstring": "~1.0", 1732 | "symfony/service-contracts": "^2.5|^3", 1733 | "symfony/string": "^7.2" 1734 | }, 1735 | "conflict": { 1736 | "symfony/dependency-injection": "<6.4", 1737 | "symfony/dotenv": "<6.4", 1738 | "symfony/event-dispatcher": "<6.4", 1739 | "symfony/lock": "<6.4", 1740 | "symfony/process": "<6.4" 1741 | }, 1742 | "provide": { 1743 | "psr/log-implementation": "1.0|2.0|3.0" 1744 | }, 1745 | "require-dev": { 1746 | "psr/log": "^1|^2|^3", 1747 | "symfony/config": "^6.4|^7.0", 1748 | "symfony/dependency-injection": "^6.4|^7.0", 1749 | "symfony/event-dispatcher": "^6.4|^7.0", 1750 | "symfony/http-foundation": "^6.4|^7.0", 1751 | "symfony/http-kernel": "^6.4|^7.0", 1752 | "symfony/lock": "^6.4|^7.0", 1753 | "symfony/messenger": "^6.4|^7.0", 1754 | "symfony/process": "^6.4|^7.0", 1755 | "symfony/stopwatch": "^6.4|^7.0", 1756 | "symfony/var-dumper": "^6.4|^7.0" 1757 | }, 1758 | "type": "library", 1759 | "autoload": { 1760 | "psr-4": { 1761 | "Symfony\\Component\\Console\\": "" 1762 | }, 1763 | "exclude-from-classmap": [ 1764 | "/Tests/" 1765 | ] 1766 | }, 1767 | "notification-url": "https://packagist.org/downloads/", 1768 | "license": [ 1769 | "MIT" 1770 | ], 1771 | "authors": [ 1772 | { 1773 | "name": "Fabien Potencier", 1774 | "email": "fabien@symfony.com" 1775 | }, 1776 | { 1777 | "name": "Symfony Community", 1778 | "homepage": "https://symfony.com/contributors" 1779 | } 1780 | ], 1781 | "description": "Eases the creation of beautiful and testable command line interfaces", 1782 | "homepage": "https://symfony.com", 1783 | "keywords": [ 1784 | "cli", 1785 | "command-line", 1786 | "console", 1787 | "terminal" 1788 | ], 1789 | "support": { 1790 | "source": "https://github.com/symfony/console/tree/v7.3.0-BETA2" 1791 | }, 1792 | "funding": [ 1793 | { 1794 | "url": "https://symfony.com/sponsor", 1795 | "type": "custom" 1796 | }, 1797 | { 1798 | "url": "https://github.com/fabpot", 1799 | "type": "github" 1800 | }, 1801 | { 1802 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1803 | "type": "tidelift" 1804 | } 1805 | ], 1806 | "time": "2025-05-09T14:50:30+00:00" 1807 | }, 1808 | { 1809 | "name": "symfony/deprecation-contracts", 1810 | "version": "v3.6.0-BETA1", 1811 | "source": { 1812 | "type": "git", 1813 | "url": "https://github.com/symfony/deprecation-contracts.git", 1814 | "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" 1815 | }, 1816 | "dist": { 1817 | "type": "zip", 1818 | "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", 1819 | "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", 1820 | "shasum": "" 1821 | }, 1822 | "require": { 1823 | "php": ">=8.1" 1824 | }, 1825 | "type": "library", 1826 | "extra": { 1827 | "thanks": { 1828 | "url": "https://github.com/symfony/contracts", 1829 | "name": "symfony/contracts" 1830 | }, 1831 | "branch-alias": { 1832 | "dev-main": "3.6-dev" 1833 | } 1834 | }, 1835 | "autoload": { 1836 | "files": [ 1837 | "function.php" 1838 | ] 1839 | }, 1840 | "notification-url": "https://packagist.org/downloads/", 1841 | "license": [ 1842 | "MIT" 1843 | ], 1844 | "authors": [ 1845 | { 1846 | "name": "Nicolas Grekas", 1847 | "email": "p@tchwork.com" 1848 | }, 1849 | { 1850 | "name": "Symfony Community", 1851 | "homepage": "https://symfony.com/contributors" 1852 | } 1853 | ], 1854 | "description": "A generic function and convention to trigger deprecation notices", 1855 | "homepage": "https://symfony.com", 1856 | "support": { 1857 | "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0-BETA1" 1858 | }, 1859 | "funding": [ 1860 | { 1861 | "url": "https://symfony.com/sponsor", 1862 | "type": "custom" 1863 | }, 1864 | { 1865 | "url": "https://github.com/fabpot", 1866 | "type": "github" 1867 | }, 1868 | { 1869 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1870 | "type": "tidelift" 1871 | } 1872 | ], 1873 | "time": "2024-09-25T14:21:43+00:00" 1874 | }, 1875 | { 1876 | "name": "symfony/polyfill-ctype", 1877 | "version": "v1.32.0", 1878 | "source": { 1879 | "type": "git", 1880 | "url": "https://github.com/symfony/polyfill-ctype.git", 1881 | "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638" 1882 | }, 1883 | "dist": { 1884 | "type": "zip", 1885 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638", 1886 | "reference": "a3cc8b044a6ea513310cbd48ef7333b384945638", 1887 | "shasum": "" 1888 | }, 1889 | "require": { 1890 | "php": ">=7.2" 1891 | }, 1892 | "provide": { 1893 | "ext-ctype": "*" 1894 | }, 1895 | "suggest": { 1896 | "ext-ctype": "For best performance" 1897 | }, 1898 | "type": "library", 1899 | "extra": { 1900 | "thanks": { 1901 | "url": "https://github.com/symfony/polyfill", 1902 | "name": "symfony/polyfill" 1903 | } 1904 | }, 1905 | "autoload": { 1906 | "files": [ 1907 | "bootstrap.php" 1908 | ], 1909 | "psr-4": { 1910 | "Symfony\\Polyfill\\Ctype\\": "" 1911 | } 1912 | }, 1913 | "notification-url": "https://packagist.org/downloads/", 1914 | "license": [ 1915 | "MIT" 1916 | ], 1917 | "authors": [ 1918 | { 1919 | "name": "Gert de Pagter", 1920 | "email": "BackEndTea@gmail.com" 1921 | }, 1922 | { 1923 | "name": "Symfony Community", 1924 | "homepage": "https://symfony.com/contributors" 1925 | } 1926 | ], 1927 | "description": "Symfony polyfill for ctype functions", 1928 | "homepage": "https://symfony.com", 1929 | "keywords": [ 1930 | "compatibility", 1931 | "ctype", 1932 | "polyfill", 1933 | "portable" 1934 | ], 1935 | "support": { 1936 | "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" 1937 | }, 1938 | "funding": [ 1939 | { 1940 | "url": "https://symfony.com/sponsor", 1941 | "type": "custom" 1942 | }, 1943 | { 1944 | "url": "https://github.com/fabpot", 1945 | "type": "github" 1946 | }, 1947 | { 1948 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 1949 | "type": "tidelift" 1950 | } 1951 | ], 1952 | "time": "2024-09-09T11:45:10+00:00" 1953 | }, 1954 | { 1955 | "name": "symfony/polyfill-intl-grapheme", 1956 | "version": "v1.32.0", 1957 | "source": { 1958 | "type": "git", 1959 | "url": "https://github.com/symfony/polyfill-intl-grapheme.git", 1960 | "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe" 1961 | }, 1962 | "dist": { 1963 | "type": "zip", 1964 | "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", 1965 | "reference": "b9123926e3b7bc2f98c02ad54f6a4b02b91a8abe", 1966 | "shasum": "" 1967 | }, 1968 | "require": { 1969 | "php": ">=7.2" 1970 | }, 1971 | "suggest": { 1972 | "ext-intl": "For best performance" 1973 | }, 1974 | "type": "library", 1975 | "extra": { 1976 | "thanks": { 1977 | "url": "https://github.com/symfony/polyfill", 1978 | "name": "symfony/polyfill" 1979 | } 1980 | }, 1981 | "autoload": { 1982 | "files": [ 1983 | "bootstrap.php" 1984 | ], 1985 | "psr-4": { 1986 | "Symfony\\Polyfill\\Intl\\Grapheme\\": "" 1987 | } 1988 | }, 1989 | "notification-url": "https://packagist.org/downloads/", 1990 | "license": [ 1991 | "MIT" 1992 | ], 1993 | "authors": [ 1994 | { 1995 | "name": "Nicolas Grekas", 1996 | "email": "p@tchwork.com" 1997 | }, 1998 | { 1999 | "name": "Symfony Community", 2000 | "homepage": "https://symfony.com/contributors" 2001 | } 2002 | ], 2003 | "description": "Symfony polyfill for intl's grapheme_* functions", 2004 | "homepage": "https://symfony.com", 2005 | "keywords": [ 2006 | "compatibility", 2007 | "grapheme", 2008 | "intl", 2009 | "polyfill", 2010 | "portable", 2011 | "shim" 2012 | ], 2013 | "support": { 2014 | "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" 2015 | }, 2016 | "funding": [ 2017 | { 2018 | "url": "https://symfony.com/sponsor", 2019 | "type": "custom" 2020 | }, 2021 | { 2022 | "url": "https://github.com/fabpot", 2023 | "type": "github" 2024 | }, 2025 | { 2026 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2027 | "type": "tidelift" 2028 | } 2029 | ], 2030 | "time": "2024-09-09T11:45:10+00:00" 2031 | }, 2032 | { 2033 | "name": "symfony/polyfill-intl-normalizer", 2034 | "version": "v1.32.0", 2035 | "source": { 2036 | "type": "git", 2037 | "url": "https://github.com/symfony/polyfill-intl-normalizer.git", 2038 | "reference": "3833d7255cc303546435cb650316bff708a1c75c" 2039 | }, 2040 | "dist": { 2041 | "type": "zip", 2042 | "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/3833d7255cc303546435cb650316bff708a1c75c", 2043 | "reference": "3833d7255cc303546435cb650316bff708a1c75c", 2044 | "shasum": "" 2045 | }, 2046 | "require": { 2047 | "php": ">=7.2" 2048 | }, 2049 | "suggest": { 2050 | "ext-intl": "For best performance" 2051 | }, 2052 | "type": "library", 2053 | "extra": { 2054 | "thanks": { 2055 | "url": "https://github.com/symfony/polyfill", 2056 | "name": "symfony/polyfill" 2057 | } 2058 | }, 2059 | "autoload": { 2060 | "files": [ 2061 | "bootstrap.php" 2062 | ], 2063 | "psr-4": { 2064 | "Symfony\\Polyfill\\Intl\\Normalizer\\": "" 2065 | }, 2066 | "classmap": [ 2067 | "Resources/stubs" 2068 | ] 2069 | }, 2070 | "notification-url": "https://packagist.org/downloads/", 2071 | "license": [ 2072 | "MIT" 2073 | ], 2074 | "authors": [ 2075 | { 2076 | "name": "Nicolas Grekas", 2077 | "email": "p@tchwork.com" 2078 | }, 2079 | { 2080 | "name": "Symfony Community", 2081 | "homepage": "https://symfony.com/contributors" 2082 | } 2083 | ], 2084 | "description": "Symfony polyfill for intl's Normalizer class and related functions", 2085 | "homepage": "https://symfony.com", 2086 | "keywords": [ 2087 | "compatibility", 2088 | "intl", 2089 | "normalizer", 2090 | "polyfill", 2091 | "portable", 2092 | "shim" 2093 | ], 2094 | "support": { 2095 | "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" 2096 | }, 2097 | "funding": [ 2098 | { 2099 | "url": "https://symfony.com/sponsor", 2100 | "type": "custom" 2101 | }, 2102 | { 2103 | "url": "https://github.com/fabpot", 2104 | "type": "github" 2105 | }, 2106 | { 2107 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2108 | "type": "tidelift" 2109 | } 2110 | ], 2111 | "time": "2024-09-09T11:45:10+00:00" 2112 | }, 2113 | { 2114 | "name": "symfony/polyfill-mbstring", 2115 | "version": "v1.32.0", 2116 | "source": { 2117 | "type": "git", 2118 | "url": "https://github.com/symfony/polyfill-mbstring.git", 2119 | "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" 2120 | }, 2121 | "dist": { 2122 | "type": "zip", 2123 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", 2124 | "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", 2125 | "shasum": "" 2126 | }, 2127 | "require": { 2128 | "ext-iconv": "*", 2129 | "php": ">=7.2" 2130 | }, 2131 | "provide": { 2132 | "ext-mbstring": "*" 2133 | }, 2134 | "suggest": { 2135 | "ext-mbstring": "For best performance" 2136 | }, 2137 | "type": "library", 2138 | "extra": { 2139 | "thanks": { 2140 | "url": "https://github.com/symfony/polyfill", 2141 | "name": "symfony/polyfill" 2142 | } 2143 | }, 2144 | "autoload": { 2145 | "files": [ 2146 | "bootstrap.php" 2147 | ], 2148 | "psr-4": { 2149 | "Symfony\\Polyfill\\Mbstring\\": "" 2150 | } 2151 | }, 2152 | "notification-url": "https://packagist.org/downloads/", 2153 | "license": [ 2154 | "MIT" 2155 | ], 2156 | "authors": [ 2157 | { 2158 | "name": "Nicolas Grekas", 2159 | "email": "p@tchwork.com" 2160 | }, 2161 | { 2162 | "name": "Symfony Community", 2163 | "homepage": "https://symfony.com/contributors" 2164 | } 2165 | ], 2166 | "description": "Symfony polyfill for the Mbstring extension", 2167 | "homepage": "https://symfony.com", 2168 | "keywords": [ 2169 | "compatibility", 2170 | "mbstring", 2171 | "polyfill", 2172 | "portable", 2173 | "shim" 2174 | ], 2175 | "support": { 2176 | "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" 2177 | }, 2178 | "funding": [ 2179 | { 2180 | "url": "https://symfony.com/sponsor", 2181 | "type": "custom" 2182 | }, 2183 | { 2184 | "url": "https://github.com/fabpot", 2185 | "type": "github" 2186 | }, 2187 | { 2188 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2189 | "type": "tidelift" 2190 | } 2191 | ], 2192 | "time": "2024-12-23T08:48:59+00:00" 2193 | }, 2194 | { 2195 | "name": "symfony/process", 2196 | "version": "v7.3.0-BETA1", 2197 | "source": { 2198 | "type": "git", 2199 | "url": "https://github.com/symfony/process.git", 2200 | "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" 2201 | }, 2202 | "dist": { 2203 | "type": "zip", 2204 | "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", 2205 | "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", 2206 | "shasum": "" 2207 | }, 2208 | "require": { 2209 | "php": ">=8.2" 2210 | }, 2211 | "type": "library", 2212 | "autoload": { 2213 | "psr-4": { 2214 | "Symfony\\Component\\Process\\": "" 2215 | }, 2216 | "exclude-from-classmap": [ 2217 | "/Tests/" 2218 | ] 2219 | }, 2220 | "notification-url": "https://packagist.org/downloads/", 2221 | "license": [ 2222 | "MIT" 2223 | ], 2224 | "authors": [ 2225 | { 2226 | "name": "Fabien Potencier", 2227 | "email": "fabien@symfony.com" 2228 | }, 2229 | { 2230 | "name": "Symfony Community", 2231 | "homepage": "https://symfony.com/contributors" 2232 | } 2233 | ], 2234 | "description": "Executes commands in sub-processes", 2235 | "homepage": "https://symfony.com", 2236 | "support": { 2237 | "source": "https://github.com/symfony/process/tree/v7.3.0-BETA1" 2238 | }, 2239 | "funding": [ 2240 | { 2241 | "url": "https://symfony.com/sponsor", 2242 | "type": "custom" 2243 | }, 2244 | { 2245 | "url": "https://github.com/fabpot", 2246 | "type": "github" 2247 | }, 2248 | { 2249 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2250 | "type": "tidelift" 2251 | } 2252 | ], 2253 | "time": "2025-04-17T09:11:12+00:00" 2254 | }, 2255 | { 2256 | "name": "symfony/service-contracts", 2257 | "version": "v3.6.0-BETA1", 2258 | "source": { 2259 | "type": "git", 2260 | "url": "https://github.com/symfony/service-contracts.git", 2261 | "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" 2262 | }, 2263 | "dist": { 2264 | "type": "zip", 2265 | "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", 2266 | "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", 2267 | "shasum": "" 2268 | }, 2269 | "require": { 2270 | "php": ">=8.1", 2271 | "psr/container": "^1.1|^2.0", 2272 | "symfony/deprecation-contracts": "^2.5|^3" 2273 | }, 2274 | "conflict": { 2275 | "ext-psr": "<1.1|>=2" 2276 | }, 2277 | "type": "library", 2278 | "extra": { 2279 | "thanks": { 2280 | "url": "https://github.com/symfony/contracts", 2281 | "name": "symfony/contracts" 2282 | }, 2283 | "branch-alias": { 2284 | "dev-main": "3.6-dev" 2285 | } 2286 | }, 2287 | "autoload": { 2288 | "psr-4": { 2289 | "Symfony\\Contracts\\Service\\": "" 2290 | }, 2291 | "exclude-from-classmap": [ 2292 | "/Test/" 2293 | ] 2294 | }, 2295 | "notification-url": "https://packagist.org/downloads/", 2296 | "license": [ 2297 | "MIT" 2298 | ], 2299 | "authors": [ 2300 | { 2301 | "name": "Nicolas Grekas", 2302 | "email": "p@tchwork.com" 2303 | }, 2304 | { 2305 | "name": "Symfony Community", 2306 | "homepage": "https://symfony.com/contributors" 2307 | } 2308 | ], 2309 | "description": "Generic abstractions related to writing services", 2310 | "homepage": "https://symfony.com", 2311 | "keywords": [ 2312 | "abstractions", 2313 | "contracts", 2314 | "decoupling", 2315 | "interfaces", 2316 | "interoperability", 2317 | "standards" 2318 | ], 2319 | "support": { 2320 | "source": "https://github.com/symfony/service-contracts/tree/v3.6.0-BETA1" 2321 | }, 2322 | "funding": [ 2323 | { 2324 | "url": "https://symfony.com/sponsor", 2325 | "type": "custom" 2326 | }, 2327 | { 2328 | "url": "https://github.com/fabpot", 2329 | "type": "github" 2330 | }, 2331 | { 2332 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2333 | "type": "tidelift" 2334 | } 2335 | ], 2336 | "time": "2025-04-25T09:37:31+00:00" 2337 | }, 2338 | { 2339 | "name": "symfony/string", 2340 | "version": "v7.3.0-BETA1", 2341 | "source": { 2342 | "type": "git", 2343 | "url": "https://github.com/symfony/string.git", 2344 | "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" 2345 | }, 2346 | "dist": { 2347 | "type": "zip", 2348 | "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", 2349 | "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", 2350 | "shasum": "" 2351 | }, 2352 | "require": { 2353 | "php": ">=8.2", 2354 | "symfony/polyfill-ctype": "~1.8", 2355 | "symfony/polyfill-intl-grapheme": "~1.0", 2356 | "symfony/polyfill-intl-normalizer": "~1.0", 2357 | "symfony/polyfill-mbstring": "~1.0" 2358 | }, 2359 | "conflict": { 2360 | "symfony/translation-contracts": "<2.5" 2361 | }, 2362 | "require-dev": { 2363 | "symfony/emoji": "^7.1", 2364 | "symfony/error-handler": "^6.4|^7.0", 2365 | "symfony/http-client": "^6.4|^7.0", 2366 | "symfony/intl": "^6.4|^7.0", 2367 | "symfony/translation-contracts": "^2.5|^3.0", 2368 | "symfony/var-exporter": "^6.4|^7.0" 2369 | }, 2370 | "type": "library", 2371 | "autoload": { 2372 | "files": [ 2373 | "Resources/functions.php" 2374 | ], 2375 | "psr-4": { 2376 | "Symfony\\Component\\String\\": "" 2377 | }, 2378 | "exclude-from-classmap": [ 2379 | "/Tests/" 2380 | ] 2381 | }, 2382 | "notification-url": "https://packagist.org/downloads/", 2383 | "license": [ 2384 | "MIT" 2385 | ], 2386 | "authors": [ 2387 | { 2388 | "name": "Nicolas Grekas", 2389 | "email": "p@tchwork.com" 2390 | }, 2391 | { 2392 | "name": "Symfony Community", 2393 | "homepage": "https://symfony.com/contributors" 2394 | } 2395 | ], 2396 | "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way", 2397 | "homepage": "https://symfony.com", 2398 | "keywords": [ 2399 | "grapheme", 2400 | "i18n", 2401 | "string", 2402 | "unicode", 2403 | "utf-8", 2404 | "utf8" 2405 | ], 2406 | "support": { 2407 | "source": "https://github.com/symfony/string/tree/v7.3.0-BETA1" 2408 | }, 2409 | "funding": [ 2410 | { 2411 | "url": "https://symfony.com/sponsor", 2412 | "type": "custom" 2413 | }, 2414 | { 2415 | "url": "https://github.com/fabpot", 2416 | "type": "github" 2417 | }, 2418 | { 2419 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2420 | "type": "tidelift" 2421 | } 2422 | ], 2423 | "time": "2025-04-20T20:19:01+00:00" 2424 | }, 2425 | { 2426 | "name": "symfony/var-dumper", 2427 | "version": "v7.3.0-BETA2", 2428 | "source": { 2429 | "type": "git", 2430 | "url": "https://github.com/symfony/var-dumper.git", 2431 | "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e" 2432 | }, 2433 | "dist": { 2434 | "type": "zip", 2435 | "url": "https://api.github.com/repos/symfony/var-dumper/zipball/548f6760c54197b1084e1e5c71f6d9d523f2f78e", 2436 | "reference": "548f6760c54197b1084e1e5c71f6d9d523f2f78e", 2437 | "shasum": "" 2438 | }, 2439 | "require": { 2440 | "php": ">=8.2", 2441 | "symfony/deprecation-contracts": "^2.5|^3", 2442 | "symfony/polyfill-mbstring": "~1.0" 2443 | }, 2444 | "conflict": { 2445 | "symfony/console": "<6.4" 2446 | }, 2447 | "require-dev": { 2448 | "ext-iconv": "*", 2449 | "symfony/console": "^6.4|^7.0", 2450 | "symfony/http-kernel": "^6.4|^7.0", 2451 | "symfony/process": "^6.4|^7.0", 2452 | "symfony/uid": "^6.4|^7.0", 2453 | "twig/twig": "^3.12" 2454 | }, 2455 | "bin": [ 2456 | "Resources/bin/var-dump-server" 2457 | ], 2458 | "type": "library", 2459 | "autoload": { 2460 | "files": [ 2461 | "Resources/functions/dump.php" 2462 | ], 2463 | "psr-4": { 2464 | "Symfony\\Component\\VarDumper\\": "" 2465 | }, 2466 | "exclude-from-classmap": [ 2467 | "/Tests/" 2468 | ] 2469 | }, 2470 | "notification-url": "https://packagist.org/downloads/", 2471 | "license": [ 2472 | "MIT" 2473 | ], 2474 | "authors": [ 2475 | { 2476 | "name": "Nicolas Grekas", 2477 | "email": "p@tchwork.com" 2478 | }, 2479 | { 2480 | "name": "Symfony Community", 2481 | "homepage": "https://symfony.com/contributors" 2482 | } 2483 | ], 2484 | "description": "Provides mechanisms for walking through any arbitrary PHP variable", 2485 | "homepage": "https://symfony.com", 2486 | "keywords": [ 2487 | "debug", 2488 | "dump" 2489 | ], 2490 | "support": { 2491 | "source": "https://github.com/symfony/var-dumper/tree/v7.3.0-BETA2" 2492 | }, 2493 | "funding": [ 2494 | { 2495 | "url": "https://symfony.com/sponsor", 2496 | "type": "custom" 2497 | }, 2498 | { 2499 | "url": "https://github.com/fabpot", 2500 | "type": "github" 2501 | }, 2502 | { 2503 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 2504 | "type": "tidelift" 2505 | } 2506 | ], 2507 | "time": "2025-04-27T18:39:23+00:00" 2508 | }, 2509 | { 2510 | "name": "theseer/tokenizer", 2511 | "version": "1.2.3", 2512 | "source": { 2513 | "type": "git", 2514 | "url": "https://github.com/theseer/tokenizer.git", 2515 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2" 2516 | }, 2517 | "dist": { 2518 | "type": "zip", 2519 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 2520 | "reference": "737eda637ed5e28c3413cb1ebe8bb52cbf1ca7a2", 2521 | "shasum": "" 2522 | }, 2523 | "require": { 2524 | "ext-dom": "*", 2525 | "ext-tokenizer": "*", 2526 | "ext-xmlwriter": "*", 2527 | "php": "^7.2 || ^8.0" 2528 | }, 2529 | "type": "library", 2530 | "autoload": { 2531 | "classmap": [ 2532 | "src/" 2533 | ] 2534 | }, 2535 | "notification-url": "https://packagist.org/downloads/", 2536 | "license": [ 2537 | "BSD-3-Clause" 2538 | ], 2539 | "authors": [ 2540 | { 2541 | "name": "Arne Blankerts", 2542 | "email": "arne@blankerts.de", 2543 | "role": "Developer" 2544 | } 2545 | ], 2546 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 2547 | "support": { 2548 | "issues": "https://github.com/theseer/tokenizer/issues", 2549 | "source": "https://github.com/theseer/tokenizer/tree/1.2.3" 2550 | }, 2551 | "funding": [ 2552 | { 2553 | "url": "https://github.com/theseer", 2554 | "type": "github" 2555 | } 2556 | ], 2557 | "time": "2024-03-03T12:36:25+00:00" 2558 | } 2559 | ], 2560 | "aliases": [], 2561 | "minimum-stability": "beta", 2562 | "stability-flags": { 2563 | "symfony/console": 10, 2564 | "symfony/process": 10, 2565 | "symfony/var-dumper": 10 2566 | }, 2567 | "prefer-stable": false, 2568 | "prefer-lowest": false, 2569 | "platform": { 2570 | "php": ">=8.4" 2571 | }, 2572 | "platform-dev": {}, 2573 | "plugin-api-version": "2.6.0" 2574 | } 2575 | -------------------------------------------------------------------------------- /phpunit.ci.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | test 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/Console.php: -------------------------------------------------------------------------------- 1 | parse($argv ?? $_SERVER['argv'], $validate); 51 | } 52 | 53 | public function __construct( 54 | public $stdout = STDOUT, 55 | public $stderr = STDERR, 56 | public $stdin = STDIN, 57 | public string $heading = '', 58 | public string $epilog = '', 59 | public ?string $commandName = null, 60 | public ArgvParser $parser = new ArgvParser(), 61 | ) { 62 | } 63 | 64 | public function addParameter( 65 | string|array $name, 66 | ParameterType $type, 67 | string $description = '', 68 | bool $required = false, 69 | mixed $default = null, 70 | bool $negatable = false, 71 | ): Parameter { 72 | return $this->parser->addParameter($name, $type, $description, $required, $default, $negatable); 73 | } 74 | 75 | public function addHelpParameter(): Parameter 76 | { 77 | return $this->addParameter('--help|-h', static::BOOLEAN, 'Show description of all parameters', false); 78 | } 79 | 80 | public function addVerbosityParameter(): Parameter 81 | { 82 | return $this->addParameter('--verbosity|-v', static::LEVEL, 'The verbosity level of the output'); 83 | } 84 | 85 | public function get(string $name, mixed $default = null): mixed 86 | { 87 | return $this->params[$name] ?? $default; 88 | } 89 | 90 | protected function configure(): void 91 | { 92 | } 93 | 94 | protected function preprocess(): void 95 | { 96 | } 97 | 98 | protected function doExecute(): int|bool 99 | { 100 | return 0; 101 | } 102 | 103 | public function execute(?array $argv = null, ?\Closure $main = null): int 104 | { 105 | $argv = $argv ?? $_SERVER['argv']; 106 | $this->commandName ??= basename($argv[0]); 107 | try { 108 | $this->disableDefaultParameters || ($this->addHelpParameter() && $this->addVerbosityParameter()); 109 | $this->configure(); 110 | $this->params = $this->parser->parse($argv, false); 111 | if (!$this->disableDefaultParameters) { 112 | $this->verbosity = (int) $this->get('verbosity'); 113 | if ($this->get('help')) { 114 | $this->showHelp(); 115 | 116 | return static::SUCCESS; 117 | } 118 | } 119 | $this->params = $this->parser->validateAndCastParams($this->params); 120 | $this->preprocess(); 121 | $exitCode = $main ? $main->call($this, $this) : $this->doExecute(); 122 | if ($exitCode === true || $exitCode === null) { 123 | $exitCode = 0; 124 | } elseif ($exitCode === false) { 125 | $exitCode = 255; 126 | } 127 | 128 | return (int) $exitCode; 129 | } catch (\Throwable $e) { 130 | return $this->handleException($e); 131 | } 132 | } 133 | 134 | public function showHelp(): void 135 | { 136 | $help = ParameterDescriptor::describe($this->parser, $this->commandName, $this->epilog); 137 | $this->writeln(ltrim($this->heading . "\n\n" . $help))->newLine(); 138 | } 139 | 140 | public function write(string $message, bool $err = false): static 141 | { 142 | fwrite($err ? $this->stderr : $this->stdout, $message); 143 | 144 | return $this; 145 | } 146 | 147 | public function writeln(string $message = '', bool $err = false): static 148 | { 149 | return $this->write($message . "\n", $err); 150 | } 151 | 152 | public function newLine(int $lines = 1, bool $err = false): static 153 | { 154 | return $this->write(str_repeat("\n", $lines), $err); 155 | } 156 | 157 | public function in(): string 158 | { 159 | return rtrim(fread(STDIN, 8192), "\n\r"); 160 | } 161 | 162 | public function ask(string $question = '', string $default = ''): string 163 | { 164 | $this->write($question); 165 | $in = rtrim(fread(STDIN, 8192), "\n\r"); 166 | 167 | return $in === '' ? $default : $in; 168 | } 169 | 170 | public function askConfirm(string $question = '', string $default = ''): bool 171 | { 172 | return (bool) $this->mapBoolean($this->ask($question, $default)); 173 | } 174 | 175 | public function mapBoolean($in): bool|null 176 | { 177 | $in = strtolower((string) $in); 178 | if (in_array($in, $this->boolMapping[0], true)) { 179 | return false; 180 | } 181 | if (in_array($in, $this->boolMapping[1], true)) { 182 | return true; 183 | } 184 | 185 | return null; 186 | } 187 | 188 | public function exec(string $cmd, \Closure|null|false $output = null, bool $showCmd = true): ExecResult 189 | { 190 | !$showCmd || $this->writeln('>> ' . $cmd); 191 | [$outFull, $errFull, $code] = ['', '', 255]; 192 | if ($process = proc_open($cmd, [["pipe", "r"], ["pipe", "w"], ["pipe", "w"]], $pipes)) { 193 | $callback = $output ?: fn($data, $err) => ($output === false) || $this->write($data, $err); 194 | while (($out = fgets($pipes[1])) || $err = fgets($pipes[2])) { 195 | if (isset($out[0])) { 196 | $callback($out, false); 197 | $outFull .= $output === false ? $out : ''; 198 | } 199 | if (isset($err[0])) { 200 | $callback($err, false); 201 | $errFull .= $output === false ? $err : ''; 202 | } 203 | } 204 | 205 | $code = proc_close($process); 206 | } 207 | 208 | return new ExecResult($code, $outFull, $errFull); 209 | } 210 | 211 | public function mustExec(string $cmd, ?\Closure $output = null): ExecResult 212 | { 213 | $result = $this->exec($cmd, $output); 214 | $result->success || throw new \RuntimeException('Command "' . $cmd . '" failed with code ' . $result->code); 215 | 216 | return $result; 217 | } 218 | 219 | protected function handleException(\Throwable $e): int 220 | { 221 | if ($e instanceof InvalidParameterException) { 222 | $this->writeln('[Warning] ' . $e->getMessage(), true)->newLine(err: true) 223 | ->writeln( 224 | $this->commandName . ' ' . ParameterDescriptor::synopsis($this->parser, false), 225 | true 226 | ); 227 | } else { 228 | $this->writeln('[Error] ' . $e->getMessage(), true); 229 | } 230 | if ($this->verbosity > 0) { 231 | $this->writeln('[Backtrace]:', true) 232 | ->writeln($e->getTraceAsString(), true); 233 | } 234 | 235 | return $e->getCode() === 0 ? 255 : $e->getCode(); 236 | } 237 | 238 | public function offsetExists(mixed $offset): bool 239 | { 240 | return array_key_exists($offset, $this->params); 241 | } 242 | 243 | public function offsetGet(mixed $offset): mixed 244 | { 245 | return $this->params[$offset] ?? null; 246 | } 247 | 248 | public function offsetSet(mixed $offset, mixed $value): void 249 | { 250 | throw new \BadMethodCallException('Cannot set params.'); 251 | } 252 | 253 | public function offsetUnset(mixed $offset): void 254 | { 255 | throw new \BadMethodCallException('Cannot unset params.'); 256 | } 257 | } 258 | 259 | class ExecResult 260 | { 261 | public bool $success { 262 | get => $this->code === 0; 263 | } 264 | 265 | public function __construct(public int $code = 0, public string $output = '', public string $errOutput = '') 266 | { 267 | } 268 | } 269 | 270 | class ArgvParser 271 | { 272 | private array $params = []; 273 | 274 | private array $tokens = []; 275 | 276 | private array $existsNames = []; 277 | 278 | private bool $parseOptions = false; 279 | 280 | public private(set) int $currentArgument = 0; 281 | 282 | /** @var array */ 283 | public private(set) array $parameters = []; 284 | 285 | /** @var array */ 286 | public array $arguments { 287 | get => array_filter($this->parameters, static fn($parameter) => $parameter->isArg); 288 | } 289 | 290 | /** @var array */ 291 | public array $options { 292 | get => array_filter($this->parameters, static fn($parameter) => !$parameter->isArg); 293 | } 294 | 295 | public function addParameter( 296 | string|array $name, 297 | ParameterType $type, 298 | string $description = '', 299 | bool $required = false, 300 | mixed $default = null, 301 | bool $negatable = false, 302 | ): Parameter { 303 | if (is_string($name) && str_contains($name, '|')) { 304 | $name = explode('|', $name); 305 | foreach ($name as $n) { 306 | if (!str_starts_with($n, '-')) { 307 | throw new \InvalidArgumentException('Argument name cannot contains "|" sign.'); 308 | } 309 | } 310 | } 311 | $parameter = new Parameter($name, $type, $description, $required, $default, $negatable); 312 | foreach ((array) $parameter->name as $n) { 313 | if (in_array($n, $this->existsNames, true)) { 314 | throw new \InvalidArgumentException('Duplicate parameter name "' . $n . '"'); 315 | } 316 | } 317 | array_push($this->existsNames, ...((array) $parameter->name)); 318 | ($this->parameters[$parameter->primaryName] = $parameter) && $parameter->selfValidate(); 319 | 320 | return $parameter; 321 | } 322 | 323 | public function removeParameter(string $name): void 324 | { 325 | unset($this->parameters[$name]); 326 | } 327 | 328 | public function getArgument(string $name): ?Parameter 329 | { 330 | return array_find($this->arguments, static fn($n) => $n === $name); 331 | } 332 | 333 | public function getArgumentByIndex(int $index): ?Parameter 334 | { 335 | return array_values($this->arguments)[$index] ?? null; 336 | } 337 | 338 | public function getLastArgument(): ?Parameter 339 | { 340 | $args = $this->arguments; 341 | 342 | return $args[array_key_last($args)] ?? null; 343 | } 344 | 345 | public function getOption(string $name): ?Parameter 346 | { 347 | return array_find($this->options, static fn(Parameter $option) => $option->hasName($name)); 348 | } 349 | 350 | public function mustGetOption(string $name): Parameter 351 | { 352 | if (!$option = $this->getOption($name)) { 353 | throw new InvalidParameterException(\sprintf('The "-%s" option does not exist.', $name)); 354 | } 355 | 356 | return $option; 357 | } 358 | 359 | public function parse(array $argv, bool $validate = true): array 360 | { 361 | foreach ($this->parameters as $parameter) { 362 | $parameter->selfValidate(); 363 | } 364 | array_shift($argv); 365 | $this->currentArgument = 0; 366 | $this->parseOptions = true; 367 | $this->params = []; 368 | $this->tokens = $argv; 369 | while (null !== $token = array_shift($this->tokens)) { 370 | $this->parseToken((string) $token); 371 | } 372 | 373 | if ($validate) { 374 | return $this->validateAndCastParams($this->params); 375 | } 376 | 377 | return $this->params; 378 | } 379 | 380 | public function validateAndCastParams(array $params): array 381 | { 382 | foreach ($this->parameters as $parameter) { 383 | if (!array_key_exists($parameter->primaryName, $params)) { 384 | $parameter->assertInput( 385 | !$parameter->isArg || !$parameter->required, 386 | "Required argument \"{$parameter->primaryName}\" is missing." 387 | ); 388 | $params[$parameter->primaryName] = $parameter->defaultValue ?? false; 389 | } else { 390 | $parameter->validate($this->params[$parameter->primaryName]); 391 | $params[$parameter->primaryName] = $parameter->castValue($params[$parameter->primaryName]); 392 | } 393 | } 394 | 395 | return $params; 396 | } 397 | 398 | protected function parseToken(string $token): void 399 | { 400 | if ($this->parseOptions && '' === $token) { 401 | $this->parseArgument($token); 402 | } elseif ($this->parseOptions && '--' === $token) { 403 | $this->parseOptions = false; 404 | } elseif ($this->parseOptions && str_starts_with($token, '--')) { 405 | $this->parseLongOption($token); 406 | } elseif ($this->parseOptions && '-' === $token[0] && '-' !== $token) { 407 | $this->parseShortOption($token); 408 | } else { 409 | $this->parseArgument($token); 410 | } 411 | } 412 | 413 | private function parseShortOption(string $token): void 414 | { 415 | $name = substr($token, 1); 416 | if (\strlen($name) > 1) { 417 | $option = $this->getOption($token); 418 | if ($option && $option->acceptValue) { 419 | $this->setOptionValue($name[0], substr($name, 1)); // -n[value] 420 | } else { 421 | $this->parseShortOptionSet($name); 422 | } 423 | } else { 424 | $this->setOptionValue($name, null); 425 | } 426 | } 427 | 428 | private function parseShortOptionSet(string $name): void 429 | { 430 | $len = \strlen($name); 431 | for ($i = 0; $i < $len; ++$i) { 432 | $option = $this->mustGetOption($name[$i]); 433 | if ($option->acceptValue) { 434 | $this->setOptionValue($option->primaryName, $i === $len - 1 ? null : substr($name, $i + 1)); 435 | break; 436 | } 437 | $this->setOptionValue($option->primaryName, null); 438 | } 439 | } 440 | 441 | private function parseLongOption(string $token): void 442 | { 443 | $name = substr($token, 2); 444 | $pos = strpos($name, '='); 445 | if ($pos !== false) { 446 | $value = substr($name, $pos + 1); 447 | $value !== '' || array_unshift($this->params, $value); 448 | $this->setOptionValue(substr($name, 0, $pos), $value); 449 | } else { 450 | $this->setOptionValue($name, null); 451 | } 452 | } 453 | 454 | private function parseArgument(string $token): void 455 | { 456 | if ($arg = $this->getArgumentByIndex($this->currentArgument)) { 457 | $this->params[$arg->primaryName] = $arg->type === ParameterType::ARRAY ? [$token] : $token; 458 | } elseif (($last = $this->getLastArgument()) && $last->type === ParameterType::ARRAY) { 459 | $this->params[$last->primaryName][] = $token; 460 | } else { 461 | throw new InvalidParameterException("Unknown argument \"$token\"."); 462 | } 463 | $this->currentArgument++; 464 | } 465 | 466 | public function setOptionValue(string $name, mixed $value = null): void 467 | { 468 | $option = $this->getOption($name); 469 | // If option not exists, make sure it is negatable 470 | if (!$option) { 471 | if (str_starts_with($name, 'no-')) { 472 | $option = $this->getOption(substr($name, 3)); 473 | if ($option->type === ParameterType::BOOLEAN && $option->negatable) { 474 | $this->params[$option->primaryName] = false; 475 | } 476 | 477 | return; 478 | } 479 | throw new InvalidParameterException(\sprintf('The "-%s" option does not exist.', $name)); 480 | } 481 | $option->assertInput($value === null || $option->acceptValue, 'Option "%s" does not accept value.'); 482 | // Try get option value from next token 483 | if (\in_array($value, ['', null], true) && $option->acceptValue && \count($this->tokens)) { 484 | $next = array_shift($this->tokens); 485 | if ((isset($next[0]) && '-' !== $next[0]) || \in_array($next, ['', null], true)) { 486 | $value = $next; 487 | } else { 488 | array_unshift($this->tokens, $next); 489 | } 490 | } 491 | if ($option->type === ParameterType::BOOLEAN) { 492 | $value = $value === null || $value; 493 | } 494 | if ($option->type === ParameterType::ARRAY) { 495 | $this->params[$option->primaryName][] = $value; 496 | } elseif ($option->type === ParameterType::LEVEL) { 497 | $this->params[$option->primaryName] ??= 0; 498 | $this->params[$option->primaryName]++; 499 | } else { 500 | $this->params[$option->primaryName] = $value; 501 | } 502 | } 503 | } 504 | 505 | /** 506 | * @method self description(string $value) 507 | * @method self required(bool $value) 508 | * @method self negatable(bool $value) 509 | * @method self default(mixed $value) 510 | */ 511 | class Parameter 512 | { 513 | public bool $isArg { 514 | get => is_string($this->name); 515 | } 516 | 517 | public string $primaryName { 518 | get => is_string($this->name) ? $this->name : $this->name[0]; 519 | } 520 | 521 | public string $synopsis { 522 | get { 523 | if (is_string($this->name)) { 524 | return $this->name; 525 | } 526 | $shorts = []; 527 | $fulls = []; 528 | foreach ($this->name as $n) { 529 | if (strlen($n) === 1) { 530 | $shorts[] = '-' . $n; 531 | } else { 532 | $fulls[] = '--' . $n; 533 | } 534 | } 535 | if ($this->negatable) { 536 | $fulls[] = '--no-' . $this->primaryName; 537 | } 538 | 539 | return implode(', ', array_filter([implode('|', $shorts), implode('|', $fulls)])); 540 | } 541 | } 542 | 543 | public bool $acceptValue { 544 | get => $this->type !== ParameterType::BOOLEAN && $this->type !== ParameterType::LEVEL && !$this->negatable; 545 | } 546 | 547 | public mixed $defaultValue { 548 | get => match ($this->type) { 549 | ParameterType::ARRAY => $this->default ?? [], 550 | ParameterType::LEVEL => $this->default ?? 0, 551 | default => $this->default, 552 | }; 553 | } 554 | 555 | public function __construct( 556 | public string|array $name, 557 | public ParameterType $type, 558 | public string $description = '', 559 | public bool $required = false, 560 | public mixed $default = null, 561 | public bool $negatable = false, 562 | ) { 563 | $this->name = is_string($this->name) && str_starts_with($this->name, '-') ? [$this->name] : $this->name; 564 | if (is_array($this->name)) { 565 | foreach ($this->name as $i => $n) { 566 | $this->assertArg(str_starts_with($n, '--') || strlen($n) <= 2); 567 | $this->name[$i] = ltrim($n, '-'); 568 | } 569 | } 570 | } 571 | 572 | public function selfValidate(): void 573 | { 574 | $this->assertArg( 575 | $this->type !== ParameterType::ARRAY || is_array($this->defaultValue), 576 | "Default value of \"%s\" must be an array." 577 | ); 578 | if ($this->isArg) { 579 | $this->assertArg(!$this->negatable, "Argument \"%s\" cannot be negatable."); 580 | $this->assertArg( 581 | $this->type !== ParameterType::BOOLEAN && $this->type !== ParameterType::LEVEL, 582 | "Argument \"%s\" cannot be type: {$this->type->name}." 583 | ); 584 | } else { 585 | $this->assertArg(!$this->negatable || !$this->required, "Negatable option \"%s\" cannot be required."); 586 | } 587 | $this->assertArg( 588 | !$this->required || $this->default === null, 589 | "Default value of \"%s\" cannot be set when required is true." 590 | ); 591 | } 592 | 593 | public function hasName(string $name): bool 594 | { 595 | $name = ltrim($name, '-'); 596 | 597 | return is_string($this->name) ? $this->name === $name : array_any($this->name, fn($n) => $n === $name); 598 | } 599 | 600 | public function castValue(mixed $value): mixed 601 | { 602 | return match ($this->type) { 603 | ParameterType::INT, ParameterType::LEVEL => (int) $value, 604 | ParameterType::NUMERIC, ParameterType::FLOAT => (float) $value, 605 | ParameterType::BOOLEAN => (bool) $value, 606 | ParameterType::ARRAY => (array) $value, 607 | default => $value, 608 | }; 609 | } 610 | 611 | public function validate(mixed $value): void 612 | { 613 | if ($value === null) { 614 | $this->assertInput(!$this->required, "Required value for \"%s\" is missing."); 615 | 616 | return; 617 | } 618 | $passed = match ($this->type) { 619 | ParameterType::INT => is_numeric($value) && ((string) (int) $value) === $value, 620 | ParameterType::FLOAT => is_numeric($value) && ((string) (float) $value) === $value, 621 | ParameterType::NUMERIC => is_numeric($value), 622 | ParameterType::BOOLEAN => is_bool($value) || $value === '1' || $value === '0', 623 | ParameterType::ARRAY => is_array($value), 624 | default => true, 625 | }; 626 | $this->assertInput($passed, "Invalid value type for \"%s\". Expected %s."); 627 | } 628 | 629 | public function assertArg(mixed $value, ?string $message = ''): void 630 | { 631 | $value || throw new \InvalidArgumentException(sprintf($message, $this->primaryName, $this->type->name)); 632 | } 633 | 634 | public function assertInput(mixed $value, ?string $message = ''): void 635 | { 636 | $value || throw new InvalidParameterException(sprintf($message, $this->primaryName, $this->type->name)); 637 | } 638 | 639 | public function __call(string $name, array $args) 640 | { 641 | if (property_exists($this, $name)) { 642 | $this->{$name} = $args[0]; 643 | $this->selfValidate(); 644 | 645 | return $this; 646 | } 647 | throw new \BadMethodCallException("Method $name() does not exist."); 648 | } 649 | } 650 | 651 | class ParameterDescriptor 652 | { 653 | public static function describe(ArgvParser $parser, string $commandName, string $epilog = ''): string 654 | { 655 | $lines[] = sprintf("Usage:\n %s %s", $commandName, static::synopsis($parser, true)); 656 | if (count($parser->arguments)) { 657 | $lines[] = "\nArguments:"; 658 | $maxColWidth = 0; 659 | foreach ($parser->arguments as $argument) { 660 | $argumentLines[] = static::describeArgument($argument, $maxColWidth); 661 | } 662 | foreach ($argumentLines ?? [] as [$start, $end]) { 663 | $lines[] = ' ' . $start . str_repeat(' ', $maxColWidth - strlen($start) + 4) . $end; 664 | } 665 | } 666 | if (count($parser->options)) { 667 | $lines[] = "\nOptions:"; 668 | $maxColWidth = 0; 669 | foreach ($parser->options as $option) { 670 | $optionLines[] = static::describeOption($option, $maxColWidth); 671 | } 672 | foreach ($optionLines ?? [] as [$start, $end]) { 673 | $lines[] = ' ' . $start . str_repeat(' ', $maxColWidth - strlen($start) + 4) . $end; 674 | } 675 | } 676 | $epilog && ($lines[] = "\nHelp:\n$epilog"); 677 | 678 | return implode("\n", $lines); 679 | } 680 | 681 | public static function describeArgument(Parameter $parameter, int &$maxWidth = 0): array 682 | { 683 | $default = !static::noDefault($parameter) ? ' [default: ' . static::format($parameter->default) . ']' : ''; 684 | $maxWidth = max($maxWidth, strlen($parameter->synopsis)); 685 | 686 | return [$parameter->synopsis, $parameter->description . $default]; 687 | } 688 | 689 | public static function describeOption(Parameter $parameter, int &$maxWidth = 0): array 690 | { 691 | $default = ($parameter->acceptValue || $parameter->negatable) && !static::noDefault($parameter) 692 | ? ' [default: ' . static::format($parameter->default) . ']' 693 | : ''; 694 | $value = '=' . strtoupper($parameter->primaryName); 695 | $value = $parameter->required ? $value : '[' . $value . ']'; 696 | $synopsis = $parameter->synopsis . ($parameter->acceptValue ? $value : ''); 697 | $maxWidth = max($maxWidth, strlen($synopsis)); 698 | 699 | return [ 700 | $synopsis, 701 | $parameter->description . $default . ($parameter->type === ParameterType::ARRAY ? ' (multiple values allowed)' : ''), 702 | ]; 703 | } 704 | 705 | public static function noDefault(Parameter $parameter): bool 706 | { 707 | return $parameter->default === null || (is_array($parameter->default) && count($parameter->default) === 0); 708 | } 709 | 710 | public static function format(mixed $value): string 711 | { 712 | return str_replace('\\\\', '\\', json_encode($value, \JSON_UNESCAPED_SLASHES | \JSON_UNESCAPED_UNICODE)); 713 | } 714 | 715 | public static function synopsis(ArgvParser $parser, bool $simple = false): string 716 | { 717 | $elements = []; 718 | if ($simple) { 719 | $elements[] = '[options]'; 720 | } else { 721 | foreach ($parser->options as $option) { 722 | $value = strtoupper($option->primaryName); 723 | $value = !$option->required ? '[' . $value . ']' : $value; 724 | $element = str_replace(', ', '|', $option->synopsis) . ($option->acceptValue ? ' ' . $value : ''); 725 | $elements[] = '[' . $element . ']'; 726 | } 727 | } 728 | if ($elements !== [] && $parser->arguments !== []) { 729 | $elements[] = '[--]'; 730 | } 731 | $tail = ''; 732 | foreach ($parser->arguments as $argument) { 733 | $element = ($argument->type === ParameterType::ARRAY ? '...' : '') . '<' . $argument->primaryName . '>'; 734 | if (!$argument->required) { 735 | $element = '[' . $element; 736 | $tail .= ']'; 737 | } 738 | $elements[] = $element; 739 | } 740 | 741 | return implode(' ', $elements) . $tail; 742 | } 743 | } 744 | 745 | enum ParameterType 746 | { 747 | case STRING; 748 | case INT; 749 | case NUMERIC; 750 | case FLOAT; 751 | case BOOLEAN; 752 | case LEVEL; 753 | case ARRAY; 754 | } 755 | 756 | class InvalidParameterException extends \RuntimeException 757 | { 758 | } 759 | } 760 | --------------------------------------------------------------------------------