├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── composer.lock ├── docs └── screenshots │ ├── aliases.png │ ├── background-color.png │ ├── hello-version-help.png │ ├── interactive-args.gif │ ├── repeat-args-missing.png │ └── repeat-help-message.png ├── examples ├── HelloWorld.php ├── ListCommand.php ├── RenderCommand.php ├── RepeatCommand.php └── tests │ ├── HelloWorldTest.php │ ├── ListCommandTest.php │ ├── RenderCommandTest.php │ └── RepeatCommandTest.php ├── phpunit.xml ├── src ├── Command.php ├── Commands │ ├── HelpCommand.php │ ├── InteractiveCommand.php │ └── VersionCommand.php ├── Config │ ├── Config.php │ └── ConfigLoader.php ├── Console │ ├── Console.php │ ├── ExceptionPrinter.php │ └── OutTransformer.php ├── Helpers │ ├── Decoders │ │ └── JsonDecoder.php │ └── SyntaxHelper.php ├── Interfaces │ ├── Config │ │ ├── ConfigInterface.php │ │ └── ConfigLoaderInterface.php │ ├── Console │ │ ├── ConsoleInterface.php │ │ └── TransformerInterface.php │ ├── Helpers │ │ └── DecoderInterface.php │ └── Template │ │ ├── TemplateInterface.php │ │ └── TemplateLoaderInterface.php ├── SubCommand.php └── Template │ ├── TemplateLoader.php │ └── Twig │ ├── TwigLoader.php │ └── TwigTemplate.php ├── tester ├── CommandTestCase.php └── Mocks │ └── Transformer.php └── tests ├── Acceptance ├── CanHaveSubCommandsTest.php ├── HandlesFilesystemTest.php ├── LoadsConfigurationTest.php ├── ParsesArgumentsTest.php ├── PrintsToConsoleTest.php ├── ReadsArgumentsInteractivelyTest.php ├── RendersTemplatesTest.php ├── ShowsHelpTest.php └── ShowsVersionTest.php ├── Unit ├── Config │ ├── ConfigLoaderTest.php │ └── ConfigTest.php └── Console │ ├── ExceptionPrinterTest.php │ └── OutTransformerTest.php ├── bootstrap.php └── resources └── templates └── hello.twig /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | 8 | [*.{php,json}] 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.php] 13 | indent_style = space 14 | indent_size = 4 15 | 16 | [*.{yml,json}] 17 | indent_style = space 18 | indent_size = 2 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | /packages 4 | *.sublime-* 5 | /about 6 | tests/_output/* 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.3 5 | - 7.4 6 | 7 | sudo: false 8 | 9 | install: 10 | - travis_retry composer install --no-interaction --prefer-source 11 | - composer require satooshi/php-coveralls 12 | 13 | script: 14 | - mkdir -p build/logs 15 | - vendor/bin/phpunit 16 | 17 | after_success: php vendor/bin/coveralls 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Amine Ben hammou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Tarsana Command 2 | 3 | [![Build Status](https://travis-ci.org/tarsana/command.svg?branch=master)](https://travis-ci.org/tarsana/command) 4 | [![Coverage Status](https://coveralls.io/repos/github/tarsana/command/badge.svg?branch=master)](https://coveralls.io/github/tarsana/command?branch=master) 5 | [![Code Quality](http://canllp.ca/scrutinizer/quality/g/tarsana/command)](https://scrutinizer-ci.com/g/tarsana/command) 6 | [![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/webneat) 7 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat)](https://github.com/tarsana/command/blob/master/LICENSE) 8 | 9 | A library to build command line applications using PHP. This is part of the [Tarsana Project](https://github.com/tarsana/specs). 10 | 11 | # Table of Contents 12 | 13 | - [Installation](#installation) 14 | 15 | - [Your First Command](#your-first-command) 16 | 17 | - [Initializing The Command](#initializing-the-command) 18 | 19 | - [Showing The Help And Version Of A Command](#showing-the-help-and-version-of-a-command) 20 | 21 | - [Reading & Writing to The Console](#reading--writing-to-the-console) 22 | 23 | - [Defining Arguments and Options](#defining-arguments-and-options) 24 | 25 | - [Reading Arguments and Options Interactively](#reading-arguments-and-options-interactively) **Since version 1.1.0** 26 | - [Handeling The Filesystem](#handeling-the-filesystem) 27 | 28 | - [Loading Configuration](#loading-configuration) **New on version 1.2.0** 29 | 30 | - [Rendering Templates](#rendering-templates) 31 | 32 | - [Adding SubCommands](#adding-sub-commands) 33 | 34 | - [Testing Commands](#testing-commands) 35 | 36 | - [What's Next](#whats-next) 37 | 38 | - [Development Notes](#development-notes) 39 | 40 | # Installation 41 | 42 | Install it using Composer 43 | 44 | ``` 45 | composer require tarsana/command 46 | ``` 47 | 48 | # Your First Command 49 | 50 | Let's write a "Hello World" command. Create a file `hello.php` with the following content: 51 | 52 | ```php 53 | console->line('Hello World'); 63 | } 64 | 65 | } 66 | 67 | (new HelloWorld)->run(); 68 | 69 | ``` 70 | 71 | Then run it from the terminal: 72 | 73 | ``` 74 | $ php hello.php 75 | Hello World 76 | ``` 77 | 78 | Congratulations, you have just written your first command :D 79 | 80 | As you see, `Tarsana\Command\Command` is a class providing the basic features of a command. Every command should extend it and implement the `execute()` method. 81 | 82 | # Initializing The Command 83 | 84 | In addition, `Command` gives the `init()` method which is used the initialize the command general attributes. Let's rewrite our `HelloWorld` command: 85 | 86 | ```php 87 | class HelloWorld extends Command { 88 | 89 | protected function init () 90 | { 91 | $this->name('Hello World') 92 | ->version('1.0.0-alpha') 93 | ->description('Shows a "Hello World" message'); 94 | } 95 | 96 | protected function execute() 97 | { 98 | $this->console->line('Hello World'); 99 | } 100 | 101 | } 102 | ``` 103 | 104 | Here we are overriding the `init()` method to define the command **name**, **version** and **description**. 105 | 106 | Note that the setter of an attribute `foo` is named `foo()` instead of `setFoo()`. I know that this is not a common convention but it makes sense for me. :P 107 | 108 | ```php 109 | $this->name('blabla'); // will set the name to 'blabla' and return $this 110 | $this->name(); // calling it without parameter will get the value of name 111 | ``` 112 | 113 | # Showing the Help and Version of a Command 114 | 115 | To show the version of a command, we use the `--version` flag (we will learn after that this is actually a sub command). We also have the `--help` to show the help message: 116 | 117 | ![Show version and help message](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/hello-version-help.png) 118 | 119 | # Reading & Writing to the Console 120 | 121 | The attribute `console` is used to handle the reading and writing operations to the console. 122 | 123 | Let's update our command to read the user name: 124 | 125 | ```php 126 | protected function execute() 127 | { 128 | $this->console->out('Your name: '); 129 | $name = $this->console->readLine(); 130 | $this->console->line("Hello {$name}"); 131 | } 132 | ``` 133 | 134 | ``` 135 | $ php hello.php 136 | Your name: Amine 137 | Hello Amine 138 | ``` 139 | 140 | - The `readLine()` method reads a line from the stdin and returns it as string. 141 | - The `out()` method writes some text to `stdout` (without a line break). 142 | - The `line()` method writes some text to `stdout` and adds a line break. 143 | - The `error()` method writes some text to `stderr` and adds a line break. 144 | 145 | The `Console` class provides some `tags` to control the output: 146 | 147 | ```php 148 | $this->console->line('Blue text on white background'); 149 | $this->console->line('White text on red background'); 150 | ``` 151 | 152 | ![Show colors in the console](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/background-color.png) 153 | 154 | The `` and `` tags allows to set the background and foreground colors of the text to be written; the `` tag resets the default values. The colors are given as numbers from the 256-color mode. 155 | 156 | ## List of supported tags 157 | 158 | - ``: Sets the foreground text to the color `$n` in 256-color mode. 159 | - ``: Sets the foreground text to the color `$n` in 256-color mode. 160 | - ``: Resets the formatting default values. 161 | - ``: Makes the text bold. 162 | - ``: Underlines the text. 163 | 164 | `Console` allows you also to define styles using aliases: 165 | 166 | ```php 167 | $this->console->alias('', ''); 168 | $this->console->alias('', ''); 169 | 170 | $this->console->line('Some text'); 171 | // is equivalent to 172 | $this->console->line('Some text'); 173 | ``` 174 | 175 | Predefined aliases are: 176 | 177 | ```php 178 | $this->console->line(' information text '); 179 | $this->console->line(' warning text '); 180 | $this->console->line(' success text '); 181 | $this->console->line(' error text '); 182 | $this->console->line(''); // prints four spaces " " 183 | $this->console->line('
'); // prints line break PHP_EOL 184 | ``` 185 | 186 | ![Console output aliases](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/aliases.png) 187 | 188 | **Note:** tags and aliases can be used in all strings printed to the console, including the command and arguments descriptions. 189 | 190 | # Defining Arguments and Options 191 | 192 | The command syntax is defined using the [Syntax](https://github.com/tarsana/syntax) library. Let's start with a command that repeats a word a number of times: 193 | 194 | ```php 195 | class RepeatCommand extends Command { 196 | 197 | protected function init () 198 | { 199 | $this->name('Repeat') 200 | ->version('1.0.0') 201 | ->description('Repeats a word a number of times') 202 | ->syntax('word: string, count: (number: 3)') 203 | ->options(['--upper']) 204 | ->describe('word', 'The word to repeat') 205 | ->describe('count', 'The number of times to repeat the word') 206 | ->describe('--upper', 'Converts the result to uppercase'); 207 | } 208 | 209 | protected function execute() 210 | { 211 | $result = str_repeat($this->args->word, $this->args->count); 212 | if ($this->option('--upper')) 213 | $result = strtoupper($result); 214 | $this->console->line($result); 215 | } 216 | 217 | } 218 | ``` 219 | 220 | We are using the method `syntax()` to define the syntax of arguments. The string given to this method follows the [rules described here](https://github.com/tarsana/syntax#rules) 221 | 222 | The `describe()` method is used to describe an argument. 223 | 224 | When you define the syntax of the command; arguments are parsed automatically and available in the `execute()` method via the `args` attribute. 225 | 226 | The `help` subcommand shows full description of the arguments and options: 227 | 228 | ![Help message example](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/repeat-help-message.png) 229 | 230 | And the result is: 231 | 232 | ``` 233 | $ php repeat.php foo 5 234 | foofoofoofoofoo 235 | $ php repeat.php bar --upper 236 | BARBARBAR 237 | ``` 238 | 239 | In the second example, the `count` argument takes automatically its default value. 240 | 241 | **Warning: Giving wrong arguments generates an error** 242 | 243 | ![Parse error example](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/repeat-args-missing.png) 244 | 245 | # Reading Arguments and Options Interactively 246 | 247 | Some commands can have long and complicated list of arguments. Defining the syntax of such command is easy thanks to [Syntax](https://github.com/tarsana/syntax) but typing the arguments in the command line becomes challenging. 248 | 249 | Let's take the following command for example: 250 | 251 | ```php 252 | class ClassGenerator extends Command { 253 | protected function init() 254 | { 255 | $this->name('Class Generator') 256 | ->version('1.0.0') 257 | ->description('Generates basic code for a class.') 258 | ->syntax(' 259 | language: string, 260 | name: string, 261 | parents: ([string]:[]), 262 | interfaces: ([string]:[]), 263 | attrs: [{ 264 | name, 265 | type, 266 | hasGetter: (boolean:true), 267 | hasSetter: (boolean:true), 268 | isStatic: (boolean:false) 269 | }], 270 | methods: ([{ 271 | name: string, 272 | type: string, 273 | args: [{ name, type, default: (string:null) |.}], 274 | isStatic: (boolean:false) 275 | }]:[]) 276 | ') 277 | ->descriptions([ 278 | 'language' => 'The programming language in which the code will be generated.', 279 | 'name' => 'The name of the class.', 280 | 'parents' => 'List of parent classes names.', 281 | 'interfaces' => 'List of implemented interfaces.', 282 | 'attrs' => 'List of attributes of the class.', 283 | 'attrs.name' => 'The name of the attribute.', 284 | 'attrs.type' => 'The type of the attribute.', 285 | 'attrs.hasGetter' => 'Generate a getter for the attribute.', 286 | 'attrs.hasSetter' => 'Generate a setter for the attribute.', 287 | 'attrs.isStatic' => 'The attribute is static.', 288 | 'methods' => 'List of methods of the class.', 289 | 'methods.name' => 'The method name.', 290 | 'methods.type' => 'The method return type.', 291 | 'methods.args' => 'List of arguments of the method.', 292 | 'methods.isStatic' => 'This method is static.' 293 | ]); 294 | } 295 | 296 | protected function execute() 297 | { 298 | $this->console->line("Generate code for the class {$this->args->name} in {$this->args->language}..."); 299 | 300 | } 301 | } 302 | ``` 303 | 304 | if you run the command using the `-i` flag, it will let you enter the arguments interactively: 305 | 306 | ![Interactive Arguments Reader](https://raw.githubusercontent.com/tarsana/command/master/docs/screenshots/interactive-args.gif) 307 | 308 | After reading all args, the command will show the command line version of the entered args: 309 | 310 | ``` 311 | > PHP User Serializable name:string:true:true:false 312 | ``` 313 | 314 | which means that running 315 | 316 | ``` 317 | $ php class.php PHP User Serializable name:string:true:true:false 318 | ``` 319 | 320 | would produce the same result. 321 | 322 | # Handling The Filesystem 323 | 324 | The `fs` attribute is an instance of `Tarsana\IO\Filesystem` that you can use to handle files and directories. [Read the documentation](https://github.com/tarsana/io#handeling-files-and-directories) for the full API. 325 | 326 | By default, the `Filesystem` instance points to the directory from which the command is run. You can also initialize it to any directory you want: 327 | 328 | ```php 329 | using Tarsana\IO\Filesystem; 330 | // ... 331 | protected function init() 332 | { 333 | $this->fs(new Filesystem('path/to/directory/you/want')); 334 | } 335 | ``` 336 | 337 | # Loading Configuration 338 | 339 | In addition to the command line arguments, the user can provide data to your command via configuration files. This is useful because it lets you define a default configuration file and lets the user change some values with a custom configuration file. 340 | 341 | Let's write an example command which have a global configuration file at `/home/user/.config.json`. It lets the user customize value via the file `config.json` in the current directory: 342 | 343 | ```php 344 | class ConfigCommand extends Command { 345 | protected function init() 346 | { 347 | // ... 348 | $this->configPaths(['/home/user/.config.json', 'config.json']); 349 | } 350 | 351 | protected function execute() 352 | { 353 | // getting a config value 354 | // assuming that $data is the merged content of the config files 355 | $this->config('name'); // returns $data['name'] 356 | $this->config('foo.bar.baz'); // returns $data['foo']['bar']['baz'] 357 | $this->config(); // returns $data 358 | } 359 | } 360 | ``` 361 | 362 | - The method `configPaths` take a list of paths, loads them and merges them into one configuration (it use `array_replace_recursive` internally). 363 | 364 | - The method `config` is used to retreive configuration values. 365 | 366 | Note that: 367 | 368 | - Only `json` files are supported as configuration files for the moment. Please open an issue or make a Pull Request to add other formats. 369 | 370 | - `configPaths` will silently ignore paths which does not exist in the filesystem. 371 | 372 | - A subcommand will always have the same configuration data as its parent command, unless `configPaths` is used to override it. 373 | 374 | # Rendering Templates 375 | 376 | The `Command` class gives also possibility to render templates. The default template engine is [Twig](https://twig.symfony.com) but you can use your favorite one by implementing the interfaces `TemplateLoaderInterface` and `TemplateInterface`. 377 | 378 | Let's make a command which renders a simple template. For this we will create two files: 379 | 380 | ``` 381 | render-hello.php 382 | templates/ 383 | hello.twig 384 | ``` 385 | 386 | **hello.twig** 387 | 388 | ``` 389 | Hello {{name}} 390 | ``` 391 | 392 | This is a simple template that print a hello message. 393 | 394 | **render-hello.php** 395 | 396 | ```php 397 | name('Renders Simple Template') 410 | ->description('Renders a simple twig template') 411 | ->syntax('name: (string:You)') 412 | ->describe('name', 'Your name') 413 | ->templatesPath(__DIR__.'/templates'); // defines the path to the templates 414 | } 415 | 416 | protected function execute() 417 | { 418 | $message = $this->template('hello') 419 | ->render([ 420 | 'name' => $this->args->name 421 | ]); 422 | 423 | $this->console->line($message); 424 | } 425 | 426 | } 427 | 428 | (new RenderHelloCommand)->run(); 429 | ``` 430 | 431 | **Result** 432 | 433 | ``` 434 | $ php render-hello.php Foo 435 | Hello Foo 436 | 437 | $ php render-hello.php 438 | Hello You 439 | ``` 440 | 441 | # Adding SubCommands 442 | 443 | You can add subcommands while initializing your command. 444 | 445 | ```php 446 | // ... 447 | protected function init() 448 | { 449 | //... 450 | // Assuming that FooCommand and BarCommand are already defined 451 | $this->command('foo', new FooCommand) 452 | ->command('bar', new BarCommand); // this erases the subcommand with key 'bar' if exists 453 | // Or set all subcommands at once (this will erase any previous subcommands) 454 | $this->commands([ 455 | 'foo' => new FooCommand, 456 | 'bar' => new BarCommand 457 | ]); 458 | 459 | // Later on you can get subcommands 460 | $this->commands(); // returns all the subcommands as key-value array 461 | $this->command('name'); // gets the subcommand with the given name 462 | // will throw an exception if the subcommand is missing 463 | $this->hasCommand('name'); // checks if a subcommand with the given name exists 464 | } 465 | ``` 466 | 467 | Now when you run 468 | 469 | ``` 470 | $ php your-script.php foo other arguments here 471 | ``` 472 | 473 | The `FooCommand` will be run with `other arguments here` as arguments. 474 | 475 | **Note:** subcommands will always have the attributes `console`, `fs` and `templatesLoader` pointing to the same objects as their parent, as long as you don't change them explicitly in the subcommand's code. 476 | 477 | # Testing Commands 478 | 479 | The class `Tarsana\Tester\CommandTestCase` extends `PHPUnit\Framework\TestCase` and adds useful methods to test Tarsana Commands. 480 | 481 | ## Testing the Input and Output 482 | 483 | Let's write a test for our `HelloWorld` command above which reads the user name than shows the hello message. 484 | 485 | ```php 486 | use Tarsana\Tester\CommandTestCase; 487 | 488 | class HelloWorldTest extends CommandTestCase { 489 | 490 | public function test_it_prints_hello() 491 | { 492 | $this->withStdin("Amine\n") 493 | ->command(new HelloWorld) 494 | ->prints("Your name:") 495 | ->prints("Hello Amine
"); 496 | } 497 | 498 | public function test_it_shows_hello_world_version() 499 | { 500 | $this->command(new HelloWorld, ['--version']) 501 | ->printsExactly("Hello World version 1.0.0-alpha
"); 502 | } 503 | 504 | } 505 | ``` 506 | 507 | ```php 508 | withStdin(string $content) : CommandTestCase; 509 | ``` 510 | 511 | Sets the content of the standard input of the command. 512 | 513 | ```php 514 | command(Command $c, array $args = []) : CommandTestCase; 515 | ``` 516 | 517 | Runs the command `$c` with the standard input and `$args` then stores its outputs for further assertions. 518 | 519 | ```php 520 | printsExactly(string $text) : CommandTestCase; 521 | prints(string $text) : CommandTestCase; 522 | printsError(string $text) : CommandTestCase; 523 | ``` 524 | 525 | - `printsExactly` asserts that the standard output of the command equals `$text`. Note that [tags](#list-of-supported-tags) are not applied to allow testing them easily. 526 | 527 | - `prints` asserts that the standard output of the command contains `$text`. 528 | 529 | - `printsError` asserts that error output of the command contains `$text`. 530 | 531 | ## Testing the Arguments and Options 532 | 533 | Let's now test the `RepeatCommand` above. 534 | 535 | ```php 536 | class RepeatCommandTest extends CommandTestCase { 537 | 538 | public function test_it_repeats_word_three_times() 539 | { 540 | $this->command(new RepeatCommand, ['foo']) 541 | ->argsEqual((object) [ 542 | 'word' => 'foo', 543 | 'count' => 3 544 | ]) 545 | ->optionsEqual([ 546 | '--upper' => false 547 | ]) 548 | ->printsExactly("foofoofoo
"); 549 | } 550 | 551 | public function test_it_repeats_word_n_times_uppercase() 552 | { 553 | $this->command(new RepeatCommand, ['bar', '5', '--upper']) 554 | ->argsEqual((object) [ 555 | 'word' => 'bar', 556 | 'count' => 5 557 | ]) 558 | ->optionsEqual([ 559 | '--upper' => true 560 | ]) 561 | ->printsExactly("BARBARBARBARBAR
"); 562 | } 563 | } 564 | ``` 565 | 566 | ```php 567 | argsEqual(object $args) : CommandTestCase; 568 | optionsEqual(array $options) : CommandTestCase; 569 | ``` 570 | 571 | Assert that the parsed arguments and options of the command are equal to the given values. 572 | 573 | ## Testing the Filesystem 574 | 575 | Let's take the following command: 576 | 577 | ```php 578 | class ListCommand extends Command { 579 | 580 | protected function init () 581 | { 582 | $this->name('List') 583 | ->version('1.0.0-alpha') 584 | ->description('Lists files and directories in the current directory.'); 585 | } 586 | 587 | protected function execute() 588 | { 589 | foreach($this->fs->find('*')->asArray() as $file) { 590 | $this->console->line($file->name()); 591 | } 592 | } 593 | 594 | } 595 | ``` 596 | 597 | The test can be written as follows: 598 | 599 | ```php 600 | class ListCommandTest extends CommandTestCase { 601 | 602 | public function test_it_lists_files_and_directories() 603 | { 604 | $this->havingFile('demo.txt', 'Some text here!') 605 | ->havingFile('doc.pdf') 606 | ->havingDir('src') 607 | ->command(new ListCommand) 608 | ->printsExactly('demo.txt
doc.pdf
src
'); 609 | } 610 | 611 | public function test_it_prints_nothing_when_no_files() 612 | { 613 | $this->command(new ListCommand) 614 | ->printsExactly(''); 615 | } 616 | } 617 | ``` 618 | 619 | ```php 620 | havingFile(string $path, string $content = '') : CommandTestCase; 621 | havingDir(string $path) : CommandTestCase; 622 | ``` 623 | 624 | The `CommandTestCase` run the command with a virtual filesystem. The methods `havingFile` and `havingDir` can be used to create files and directories on that filesystem before running the command. 625 | 626 | # What's Next 627 | 628 | Please take a look at the examples in the `examples` directory, and try using the library to build some awesome commands. Any feedback is welcome! 629 | 630 | # Development Notes 631 | 632 | - **Version 2.0.0** Tarsana Command now uses PHPUnit 9 and thus requires PHP 7.3 or PHP 7.4. 633 | 634 | - **Version 1.2.1** The `CommandTestCase` is now an abstract class to avoid PHPUnit warnings. 635 | 636 | - **Version 1.2.0** Commands can now load configuration from multiple JSON files. 637 | 638 | - **Version 1.1.1** Fixed a bug with subcommands not having the default `--help`, `--version` and `-i` subcommands. 639 | 640 | - **Version 1.1.0** The flag `-i` added to commands to enable interactive reading of arguments and options. 641 | 642 | - **Version 1.0.1** Fixed a bug of subcommands having different instances of `fs` and `templatesLoader` from their parent. 643 | 644 | - **Version 1.0.0** The first version is finally out; have fun! 645 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tarsana/command", 3 | "description": "A framework to build command line applications and share them with the world", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Amine Ben hammou", 9 | "email": "webneat@gmail.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "Tarsana\\Command\\": "src", 15 | "Tarsana\\Tester\\": "tester", 16 | "Tarsana\\Command\\Tests\\": "tests", 17 | "Tarsana\\Command\\Examples\\": "examples" 18 | } 19 | }, 20 | "require-dev": { 21 | "phpunit/phpunit": "^9.3" 22 | }, 23 | "require": { 24 | "php": ">=7.3", 25 | "tarsana/io": "^2.0", 26 | "tarsana/syntax": "^2.0", 27 | "twig/twig": "^2.4" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /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": "5116bdf27ea3e3dba2705e8cdb958686", 8 | "packages": [ 9 | { 10 | "name": "symfony/polyfill-ctype", 11 | "version": "v1.24.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/symfony/polyfill-ctype.git", 15 | "reference": "30885182c981ab175d4d034db0f6f469898070ab" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", 20 | "reference": "30885182c981ab175d4d034db0f6f469898070ab", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=7.1" 25 | }, 26 | "provide": { 27 | "ext-ctype": "*" 28 | }, 29 | "suggest": { 30 | "ext-ctype": "For best performance" 31 | }, 32 | "type": "library", 33 | "extra": { 34 | "branch-alias": { 35 | "dev-main": "1.23-dev" 36 | }, 37 | "thanks": { 38 | "name": "symfony/polyfill", 39 | "url": "https://github.com/symfony/polyfill" 40 | } 41 | }, 42 | "autoload": { 43 | "psr-4": { 44 | "Symfony\\Polyfill\\Ctype\\": "" 45 | }, 46 | "files": [ 47 | "bootstrap.php" 48 | ] 49 | }, 50 | "notification-url": "https://packagist.org/downloads/", 51 | "license": [ 52 | "MIT" 53 | ], 54 | "authors": [ 55 | { 56 | "name": "Gert de Pagter", 57 | "email": "BackEndTea@gmail.com" 58 | }, 59 | { 60 | "name": "Symfony Community", 61 | "homepage": "https://symfony.com/contributors" 62 | } 63 | ], 64 | "description": "Symfony polyfill for ctype functions", 65 | "homepage": "https://symfony.com", 66 | "keywords": [ 67 | "compatibility", 68 | "ctype", 69 | "polyfill", 70 | "portable" 71 | ], 72 | "funding": [ 73 | { 74 | "url": "https://symfony.com/sponsor", 75 | "type": "custom" 76 | }, 77 | { 78 | "url": "https://github.com/fabpot", 79 | "type": "github" 80 | }, 81 | { 82 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 83 | "type": "tidelift" 84 | } 85 | ], 86 | "time": "2021-10-20T20:35:02+00:00" 87 | }, 88 | { 89 | "name": "symfony/polyfill-mbstring", 90 | "version": "v1.24.0", 91 | "source": { 92 | "type": "git", 93 | "url": "https://github.com/symfony/polyfill-mbstring.git", 94 | "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" 95 | }, 96 | "dist": { 97 | "type": "zip", 98 | "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", 99 | "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", 100 | "shasum": "" 101 | }, 102 | "require": { 103 | "php": ">=7.1" 104 | }, 105 | "provide": { 106 | "ext-mbstring": "*" 107 | }, 108 | "suggest": { 109 | "ext-mbstring": "For best performance" 110 | }, 111 | "type": "library", 112 | "extra": { 113 | "branch-alias": { 114 | "dev-main": "1.23-dev" 115 | }, 116 | "thanks": { 117 | "name": "symfony/polyfill", 118 | "url": "https://github.com/symfony/polyfill" 119 | } 120 | }, 121 | "autoload": { 122 | "psr-4": { 123 | "Symfony\\Polyfill\\Mbstring\\": "" 124 | }, 125 | "files": [ 126 | "bootstrap.php" 127 | ] 128 | }, 129 | "notification-url": "https://packagist.org/downloads/", 130 | "license": [ 131 | "MIT" 132 | ], 133 | "authors": [ 134 | { 135 | "name": "Nicolas Grekas", 136 | "email": "p@tchwork.com" 137 | }, 138 | { 139 | "name": "Symfony Community", 140 | "homepage": "https://symfony.com/contributors" 141 | } 142 | ], 143 | "description": "Symfony polyfill for the Mbstring extension", 144 | "homepage": "https://symfony.com", 145 | "keywords": [ 146 | "compatibility", 147 | "mbstring", 148 | "polyfill", 149 | "portable", 150 | "shim" 151 | ], 152 | "funding": [ 153 | { 154 | "url": "https://symfony.com/sponsor", 155 | "type": "custom" 156 | }, 157 | { 158 | "url": "https://github.com/fabpot", 159 | "type": "github" 160 | }, 161 | { 162 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 163 | "type": "tidelift" 164 | } 165 | ], 166 | "time": "2021-11-30T18:21:41+00:00" 167 | }, 168 | { 169 | "name": "symfony/polyfill-php72", 170 | "version": "v1.24.0", 171 | "source": { 172 | "type": "git", 173 | "url": "https://github.com/symfony/polyfill-php72.git", 174 | "reference": "9a142215a36a3888e30d0a9eeea9766764e96976" 175 | }, 176 | "dist": { 177 | "type": "zip", 178 | "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/9a142215a36a3888e30d0a9eeea9766764e96976", 179 | "reference": "9a142215a36a3888e30d0a9eeea9766764e96976", 180 | "shasum": "" 181 | }, 182 | "require": { 183 | "php": ">=7.1" 184 | }, 185 | "type": "library", 186 | "extra": { 187 | "branch-alias": { 188 | "dev-main": "1.23-dev" 189 | }, 190 | "thanks": { 191 | "name": "symfony/polyfill", 192 | "url": "https://github.com/symfony/polyfill" 193 | } 194 | }, 195 | "autoload": { 196 | "files": [ 197 | "bootstrap.php" 198 | ], 199 | "psr-4": { 200 | "Symfony\\Polyfill\\Php72\\": "" 201 | } 202 | }, 203 | "notification-url": "https://packagist.org/downloads/", 204 | "license": [ 205 | "MIT" 206 | ], 207 | "authors": [ 208 | { 209 | "name": "Nicolas Grekas", 210 | "email": "p@tchwork.com" 211 | }, 212 | { 213 | "name": "Symfony Community", 214 | "homepage": "https://symfony.com/contributors" 215 | } 216 | ], 217 | "description": "Symfony polyfill backporting some PHP 7.2+ features to lower PHP versions", 218 | "homepage": "https://symfony.com", 219 | "keywords": [ 220 | "compatibility", 221 | "polyfill", 222 | "portable", 223 | "shim" 224 | ], 225 | "funding": [ 226 | { 227 | "url": "https://symfony.com/sponsor", 228 | "type": "custom" 229 | }, 230 | { 231 | "url": "https://github.com/fabpot", 232 | "type": "github" 233 | }, 234 | { 235 | "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", 236 | "type": "tidelift" 237 | } 238 | ], 239 | "time": "2021-05-27T09:17:38+00:00" 240 | }, 241 | { 242 | "name": "tarsana/io", 243 | "version": "2.0.1", 244 | "source": { 245 | "type": "git", 246 | "url": "https://github.com/tarsana/io.git", 247 | "reference": "8d9d95100061acc6740b34e92e7de49bf7f367d0" 248 | }, 249 | "dist": { 250 | "type": "zip", 251 | "url": "https://api.github.com/repos/tarsana/io/zipball/8d9d95100061acc6740b34e92e7de49bf7f367d0", 252 | "reference": "8d9d95100061acc6740b34e92e7de49bf7f367d0", 253 | "shasum": "" 254 | }, 255 | "require-dev": { 256 | "phpunit/phpunit": "^6.3" 257 | }, 258 | "type": "library", 259 | "autoload": { 260 | "psr-4": { 261 | "Tarsana\\IO\\": "src/" 262 | } 263 | }, 264 | "notification-url": "https://packagist.org/downloads/", 265 | "license": [ 266 | "MIT" 267 | ], 268 | "authors": [ 269 | { 270 | "name": "Amine Ben hammou", 271 | "email": "webneat@gmail.com" 272 | } 273 | ], 274 | "description": "Tarsana IO library", 275 | "time": "2018-02-10T17:22:26+00:00" 276 | }, 277 | { 278 | "name": "tarsana/syntax", 279 | "version": "2.1.0", 280 | "source": { 281 | "type": "git", 282 | "url": "https://github.com/tarsana/syntax.git", 283 | "reference": "fd3f8a25e3be0e391c8fd486ccfcffe913ade534" 284 | }, 285 | "dist": { 286 | "type": "zip", 287 | "url": "https://api.github.com/repos/tarsana/syntax/zipball/fd3f8a25e3be0e391c8fd486ccfcffe913ade534", 288 | "reference": "fd3f8a25e3be0e391c8fd486ccfcffe913ade534", 289 | "shasum": "" 290 | }, 291 | "require": { 292 | "php": "^7.0" 293 | }, 294 | "require-dev": { 295 | "phpunit/phpunit": "^6.1" 296 | }, 297 | "type": "library", 298 | "autoload": { 299 | "psr-4": { 300 | "Tarsana\\Syntax\\": "src/", 301 | "Tarsana\\Syntax\\UnitTests\\": "tests/" 302 | } 303 | }, 304 | "notification-url": "https://packagist.org/downloads/", 305 | "license": [ 306 | "MIT" 307 | ], 308 | "authors": [ 309 | { 310 | "name": "Amine Ben hammou", 311 | "email": "webneat@gmail.com" 312 | } 313 | ], 314 | "description": "A tool to encode and decode strings based on flexible and composable syntax definitions.", 315 | "time": "2017-09-08T04:27:25+00:00" 316 | }, 317 | { 318 | "name": "twig/twig", 319 | "version": "v2.14.11", 320 | "source": { 321 | "type": "git", 322 | "url": "https://github.com/twigphp/Twig.git", 323 | "reference": "66baa66f29ee30e487e05f1679903e36eb01d727" 324 | }, 325 | "dist": { 326 | "type": "zip", 327 | "url": "https://api.github.com/repos/twigphp/Twig/zipball/66baa66f29ee30e487e05f1679903e36eb01d727", 328 | "reference": "66baa66f29ee30e487e05f1679903e36eb01d727", 329 | "shasum": "" 330 | }, 331 | "require": { 332 | "php": ">=7.1.3", 333 | "symfony/polyfill-ctype": "^1.8", 334 | "symfony/polyfill-mbstring": "^1.3", 335 | "symfony/polyfill-php72": "^1.8" 336 | }, 337 | "require-dev": { 338 | "psr/container": "^1.0", 339 | "symfony/phpunit-bridge": "^4.4.9|^5.0.9|^6.0" 340 | }, 341 | "type": "library", 342 | "extra": { 343 | "branch-alias": { 344 | "dev-master": "2.14-dev" 345 | } 346 | }, 347 | "autoload": { 348 | "psr-0": { 349 | "Twig_": "lib/" 350 | }, 351 | "psr-4": { 352 | "Twig\\": "src/" 353 | } 354 | }, 355 | "notification-url": "https://packagist.org/downloads/", 356 | "license": [ 357 | "BSD-3-Clause" 358 | ], 359 | "authors": [ 360 | { 361 | "name": "Fabien Potencier", 362 | "email": "fabien@symfony.com", 363 | "homepage": "http://fabien.potencier.org", 364 | "role": "Lead Developer" 365 | }, 366 | { 367 | "name": "Twig Team", 368 | "role": "Contributors" 369 | }, 370 | { 371 | "name": "Armin Ronacher", 372 | "email": "armin.ronacher@active-4.com", 373 | "role": "Project Founder" 374 | } 375 | ], 376 | "description": "Twig, the flexible, fast, and secure template language for PHP", 377 | "homepage": "https://twig.symfony.com", 378 | "keywords": [ 379 | "templating" 380 | ], 381 | "funding": [ 382 | { 383 | "url": "https://github.com/fabpot", 384 | "type": "github" 385 | }, 386 | { 387 | "url": "https://tidelift.com/funding/github/packagist/twig/twig", 388 | "type": "tidelift" 389 | } 390 | ], 391 | "time": "2022-02-04T06:57:25+00:00" 392 | } 393 | ], 394 | "packages-dev": [ 395 | { 396 | "name": "doctrine/instantiator", 397 | "version": "1.3.1", 398 | "source": { 399 | "type": "git", 400 | "url": "https://github.com/doctrine/instantiator.git", 401 | "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" 402 | }, 403 | "dist": { 404 | "type": "zip", 405 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", 406 | "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", 407 | "shasum": "" 408 | }, 409 | "require": { 410 | "php": "^7.1 || ^8.0" 411 | }, 412 | "require-dev": { 413 | "doctrine/coding-standard": "^6.0", 414 | "ext-pdo": "*", 415 | "ext-phar": "*", 416 | "phpbench/phpbench": "^0.13", 417 | "phpstan/phpstan-phpunit": "^0.11", 418 | "phpstan/phpstan-shim": "^0.11", 419 | "phpunit/phpunit": "^7.0" 420 | }, 421 | "type": "library", 422 | "extra": { 423 | "branch-alias": { 424 | "dev-master": "1.2.x-dev" 425 | } 426 | }, 427 | "autoload": { 428 | "psr-4": { 429 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 430 | } 431 | }, 432 | "notification-url": "https://packagist.org/downloads/", 433 | "license": [ 434 | "MIT" 435 | ], 436 | "authors": [ 437 | { 438 | "name": "Marco Pivetta", 439 | "email": "ocramius@gmail.com", 440 | "homepage": "http://ocramius.github.com/" 441 | } 442 | ], 443 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 444 | "homepage": "https://www.doctrine-project.org/projects/instantiator.html", 445 | "keywords": [ 446 | "constructor", 447 | "instantiate" 448 | ], 449 | "funding": [ 450 | { 451 | "url": "https://www.doctrine-project.org/sponsorship.html", 452 | "type": "custom" 453 | }, 454 | { 455 | "url": "https://www.patreon.com/phpdoctrine", 456 | "type": "patreon" 457 | }, 458 | { 459 | "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", 460 | "type": "tidelift" 461 | } 462 | ], 463 | "time": "2020-05-29T17:27:14+00:00" 464 | }, 465 | { 466 | "name": "myclabs/deep-copy", 467 | "version": "1.10.1", 468 | "source": { 469 | "type": "git", 470 | "url": "https://github.com/myclabs/DeepCopy.git", 471 | "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" 472 | }, 473 | "dist": { 474 | "type": "zip", 475 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", 476 | "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", 477 | "shasum": "" 478 | }, 479 | "require": { 480 | "php": "^7.1 || ^8.0" 481 | }, 482 | "replace": { 483 | "myclabs/deep-copy": "self.version" 484 | }, 485 | "require-dev": { 486 | "doctrine/collections": "^1.0", 487 | "doctrine/common": "^2.6", 488 | "phpunit/phpunit": "^7.1" 489 | }, 490 | "type": "library", 491 | "autoload": { 492 | "files": [ 493 | "src/DeepCopy/deep_copy.php" 494 | ], 495 | "psr-4": { 496 | "DeepCopy\\": "src/DeepCopy/" 497 | } 498 | }, 499 | "notification-url": "https://packagist.org/downloads/", 500 | "license": [ 501 | "MIT" 502 | ], 503 | "description": "Create deep copies (clones) of your objects", 504 | "keywords": [ 505 | "clone", 506 | "copy", 507 | "duplicate", 508 | "object", 509 | "object graph" 510 | ], 511 | "funding": [ 512 | { 513 | "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", 514 | "type": "tidelift" 515 | } 516 | ], 517 | "time": "2020-06-29T13:22:24+00:00" 518 | }, 519 | { 520 | "name": "nikic/php-parser", 521 | "version": "v4.10.1", 522 | "source": { 523 | "type": "git", 524 | "url": "https://github.com/nikic/PHP-Parser.git", 525 | "reference": "1b479e7592812411c20c34d9ed33db3957bde66e" 526 | }, 527 | "dist": { 528 | "type": "zip", 529 | "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/1b479e7592812411c20c34d9ed33db3957bde66e", 530 | "reference": "1b479e7592812411c20c34d9ed33db3957bde66e", 531 | "shasum": "" 532 | }, 533 | "require": { 534 | "ext-tokenizer": "*", 535 | "php": ">=7.0" 536 | }, 537 | "require-dev": { 538 | "ircmaxell/php-yacc": "^0.0.7", 539 | "phpunit/phpunit": "^6.5 || ^7.0 || ^8.0 || ^9.0" 540 | }, 541 | "bin": [ 542 | "bin/php-parse" 543 | ], 544 | "type": "library", 545 | "extra": { 546 | "branch-alias": { 547 | "dev-master": "4.9-dev" 548 | } 549 | }, 550 | "autoload": { 551 | "psr-4": { 552 | "PhpParser\\": "lib/PhpParser" 553 | } 554 | }, 555 | "notification-url": "https://packagist.org/downloads/", 556 | "license": [ 557 | "BSD-3-Clause" 558 | ], 559 | "authors": [ 560 | { 561 | "name": "Nikita Popov" 562 | } 563 | ], 564 | "description": "A PHP parser written in PHP", 565 | "keywords": [ 566 | "parser", 567 | "php" 568 | ], 569 | "time": "2020-09-23T18:23:49+00:00" 570 | }, 571 | { 572 | "name": "phar-io/manifest", 573 | "version": "2.0.1", 574 | "source": { 575 | "type": "git", 576 | "url": "https://github.com/phar-io/manifest.git", 577 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133" 578 | }, 579 | "dist": { 580 | "type": "zip", 581 | "url": "https://api.github.com/repos/phar-io/manifest/zipball/85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 582 | "reference": "85265efd3af7ba3ca4b2a2c34dbfc5788dd29133", 583 | "shasum": "" 584 | }, 585 | "require": { 586 | "ext-dom": "*", 587 | "ext-phar": "*", 588 | "ext-xmlwriter": "*", 589 | "phar-io/version": "^3.0.1", 590 | "php": "^7.2 || ^8.0" 591 | }, 592 | "type": "library", 593 | "extra": { 594 | "branch-alias": { 595 | "dev-master": "2.0.x-dev" 596 | } 597 | }, 598 | "autoload": { 599 | "classmap": [ 600 | "src/" 601 | ] 602 | }, 603 | "notification-url": "https://packagist.org/downloads/", 604 | "license": [ 605 | "BSD-3-Clause" 606 | ], 607 | "authors": [ 608 | { 609 | "name": "Arne Blankerts", 610 | "email": "arne@blankerts.de", 611 | "role": "Developer" 612 | }, 613 | { 614 | "name": "Sebastian Heuer", 615 | "email": "sebastian@phpeople.de", 616 | "role": "Developer" 617 | }, 618 | { 619 | "name": "Sebastian Bergmann", 620 | "email": "sebastian@phpunit.de", 621 | "role": "Developer" 622 | } 623 | ], 624 | "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", 625 | "time": "2020-06-27T14:33:11+00:00" 626 | }, 627 | { 628 | "name": "phar-io/version", 629 | "version": "3.0.2", 630 | "source": { 631 | "type": "git", 632 | "url": "https://github.com/phar-io/version.git", 633 | "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0" 634 | }, 635 | "dist": { 636 | "type": "zip", 637 | "url": "https://api.github.com/repos/phar-io/version/zipball/c6bb6825def89e0a32220f88337f8ceaf1975fa0", 638 | "reference": "c6bb6825def89e0a32220f88337f8ceaf1975fa0", 639 | "shasum": "" 640 | }, 641 | "require": { 642 | "php": "^7.2 || ^8.0" 643 | }, 644 | "type": "library", 645 | "autoload": { 646 | "classmap": [ 647 | "src/" 648 | ] 649 | }, 650 | "notification-url": "https://packagist.org/downloads/", 651 | "license": [ 652 | "BSD-3-Clause" 653 | ], 654 | "authors": [ 655 | { 656 | "name": "Arne Blankerts", 657 | "email": "arne@blankerts.de", 658 | "role": "Developer" 659 | }, 660 | { 661 | "name": "Sebastian Heuer", 662 | "email": "sebastian@phpeople.de", 663 | "role": "Developer" 664 | }, 665 | { 666 | "name": "Sebastian Bergmann", 667 | "email": "sebastian@phpunit.de", 668 | "role": "Developer" 669 | } 670 | ], 671 | "description": "Library for handling version information and constraints", 672 | "time": "2020-06-27T14:39:04+00:00" 673 | }, 674 | { 675 | "name": "phpdocumentor/reflection-common", 676 | "version": "2.2.0", 677 | "source": { 678 | "type": "git", 679 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 680 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" 681 | }, 682 | "dist": { 683 | "type": "zip", 684 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", 685 | "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", 686 | "shasum": "" 687 | }, 688 | "require": { 689 | "php": "^7.2 || ^8.0" 690 | }, 691 | "type": "library", 692 | "extra": { 693 | "branch-alias": { 694 | "dev-2.x": "2.x-dev" 695 | } 696 | }, 697 | "autoload": { 698 | "psr-4": { 699 | "phpDocumentor\\Reflection\\": "src/" 700 | } 701 | }, 702 | "notification-url": "https://packagist.org/downloads/", 703 | "license": [ 704 | "MIT" 705 | ], 706 | "authors": [ 707 | { 708 | "name": "Jaap van Otterdijk", 709 | "email": "opensource@ijaap.nl" 710 | } 711 | ], 712 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 713 | "homepage": "http://www.phpdoc.org", 714 | "keywords": [ 715 | "FQSEN", 716 | "phpDocumentor", 717 | "phpdoc", 718 | "reflection", 719 | "static analysis" 720 | ], 721 | "time": "2020-06-27T09:03:43+00:00" 722 | }, 723 | { 724 | "name": "phpdocumentor/reflection-docblock", 725 | "version": "5.2.2", 726 | "source": { 727 | "type": "git", 728 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 729 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556" 730 | }, 731 | "dist": { 732 | "type": "zip", 733 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/069a785b2141f5bcf49f3e353548dc1cce6df556", 734 | "reference": "069a785b2141f5bcf49f3e353548dc1cce6df556", 735 | "shasum": "" 736 | }, 737 | "require": { 738 | "ext-filter": "*", 739 | "php": "^7.2 || ^8.0", 740 | "phpdocumentor/reflection-common": "^2.2", 741 | "phpdocumentor/type-resolver": "^1.3", 742 | "webmozart/assert": "^1.9.1" 743 | }, 744 | "require-dev": { 745 | "mockery/mockery": "~1.3.2" 746 | }, 747 | "type": "library", 748 | "extra": { 749 | "branch-alias": { 750 | "dev-master": "5.x-dev" 751 | } 752 | }, 753 | "autoload": { 754 | "psr-4": { 755 | "phpDocumentor\\Reflection\\": "src" 756 | } 757 | }, 758 | "notification-url": "https://packagist.org/downloads/", 759 | "license": [ 760 | "MIT" 761 | ], 762 | "authors": [ 763 | { 764 | "name": "Mike van Riel", 765 | "email": "me@mikevanriel.com" 766 | }, 767 | { 768 | "name": "Jaap van Otterdijk", 769 | "email": "account@ijaap.nl" 770 | } 771 | ], 772 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 773 | "time": "2020-09-03T19:13:55+00:00" 774 | }, 775 | { 776 | "name": "phpdocumentor/type-resolver", 777 | "version": "1.4.0", 778 | "source": { 779 | "type": "git", 780 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 781 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0" 782 | }, 783 | "dist": { 784 | "type": "zip", 785 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 786 | "reference": "6a467b8989322d92aa1c8bf2bebcc6e5c2ba55c0", 787 | "shasum": "" 788 | }, 789 | "require": { 790 | "php": "^7.2 || ^8.0", 791 | "phpdocumentor/reflection-common": "^2.0" 792 | }, 793 | "require-dev": { 794 | "ext-tokenizer": "*" 795 | }, 796 | "type": "library", 797 | "extra": { 798 | "branch-alias": { 799 | "dev-1.x": "1.x-dev" 800 | } 801 | }, 802 | "autoload": { 803 | "psr-4": { 804 | "phpDocumentor\\Reflection\\": "src" 805 | } 806 | }, 807 | "notification-url": "https://packagist.org/downloads/", 808 | "license": [ 809 | "MIT" 810 | ], 811 | "authors": [ 812 | { 813 | "name": "Mike van Riel", 814 | "email": "me@mikevanriel.com" 815 | } 816 | ], 817 | "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", 818 | "time": "2020-09-17T18:55:26+00:00" 819 | }, 820 | { 821 | "name": "phpspec/prophecy", 822 | "version": "1.11.1", 823 | "source": { 824 | "type": "git", 825 | "url": "https://github.com/phpspec/prophecy.git", 826 | "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" 827 | }, 828 | "dist": { 829 | "type": "zip", 830 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", 831 | "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", 832 | "shasum": "" 833 | }, 834 | "require": { 835 | "doctrine/instantiator": "^1.2", 836 | "php": "^7.2", 837 | "phpdocumentor/reflection-docblock": "^5.0", 838 | "sebastian/comparator": "^3.0 || ^4.0", 839 | "sebastian/recursion-context": "^3.0 || ^4.0" 840 | }, 841 | "require-dev": { 842 | "phpspec/phpspec": "^6.0", 843 | "phpunit/phpunit": "^8.0" 844 | }, 845 | "type": "library", 846 | "extra": { 847 | "branch-alias": { 848 | "dev-master": "1.11.x-dev" 849 | } 850 | }, 851 | "autoload": { 852 | "psr-4": { 853 | "Prophecy\\": "src/Prophecy" 854 | } 855 | }, 856 | "notification-url": "https://packagist.org/downloads/", 857 | "license": [ 858 | "MIT" 859 | ], 860 | "authors": [ 861 | { 862 | "name": "Konstantin Kudryashov", 863 | "email": "ever.zet@gmail.com", 864 | "homepage": "http://everzet.com" 865 | }, 866 | { 867 | "name": "Marcello Duarte", 868 | "email": "marcello.duarte@gmail.com" 869 | } 870 | ], 871 | "description": "Highly opinionated mocking framework for PHP 5.3+", 872 | "homepage": "https://github.com/phpspec/prophecy", 873 | "keywords": [ 874 | "Double", 875 | "Dummy", 876 | "fake", 877 | "mock", 878 | "spy", 879 | "stub" 880 | ], 881 | "time": "2020-07-08T12:44:21+00:00" 882 | }, 883 | { 884 | "name": "phpunit/php-code-coverage", 885 | "version": "9.1.11", 886 | "source": { 887 | "type": "git", 888 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 889 | "reference": "c9394cb9d07ecfa9351b96f2e296bad473195f4d" 890 | }, 891 | "dist": { 892 | "type": "zip", 893 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/c9394cb9d07ecfa9351b96f2e296bad473195f4d", 894 | "reference": "c9394cb9d07ecfa9351b96f2e296bad473195f4d", 895 | "shasum": "" 896 | }, 897 | "require": { 898 | "ext-dom": "*", 899 | "ext-libxml": "*", 900 | "ext-xmlwriter": "*", 901 | "nikic/php-parser": "^4.8", 902 | "php": ">=7.3", 903 | "phpunit/php-file-iterator": "^3.0.3", 904 | "phpunit/php-text-template": "^2.0.2", 905 | "sebastian/code-unit-reverse-lookup": "^2.0.2", 906 | "sebastian/complexity": "^2.0", 907 | "sebastian/environment": "^5.1.2", 908 | "sebastian/lines-of-code": "^1.0", 909 | "sebastian/version": "^3.0.1", 910 | "theseer/tokenizer": "^1.2.0" 911 | }, 912 | "require-dev": { 913 | "phpunit/phpunit": "^9.3" 914 | }, 915 | "suggest": { 916 | "ext-pcov": "*", 917 | "ext-xdebug": "*" 918 | }, 919 | "type": "library", 920 | "extra": { 921 | "branch-alias": { 922 | "dev-master": "9.1-dev" 923 | } 924 | }, 925 | "autoload": { 926 | "classmap": [ 927 | "src/" 928 | ] 929 | }, 930 | "notification-url": "https://packagist.org/downloads/", 931 | "license": [ 932 | "BSD-3-Clause" 933 | ], 934 | "authors": [ 935 | { 936 | "name": "Sebastian Bergmann", 937 | "email": "sebastian@phpunit.de", 938 | "role": "lead" 939 | } 940 | ], 941 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 942 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 943 | "keywords": [ 944 | "coverage", 945 | "testing", 946 | "xunit" 947 | ], 948 | "funding": [ 949 | { 950 | "url": "https://github.com/sebastianbergmann", 951 | "type": "github" 952 | } 953 | ], 954 | "time": "2020-09-19T05:29:17+00:00" 955 | }, 956 | { 957 | "name": "phpunit/php-file-iterator", 958 | "version": "3.0.4", 959 | "source": { 960 | "type": "git", 961 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 962 | "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e" 963 | }, 964 | "dist": { 965 | "type": "zip", 966 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/25fefc5b19835ca653877fe081644a3f8c1d915e", 967 | "reference": "25fefc5b19835ca653877fe081644a3f8c1d915e", 968 | "shasum": "" 969 | }, 970 | "require": { 971 | "php": "^7.3 || ^8.0" 972 | }, 973 | "require-dev": { 974 | "phpunit/phpunit": "^9.0" 975 | }, 976 | "type": "library", 977 | "extra": { 978 | "branch-alias": { 979 | "dev-master": "3.0-dev" 980 | } 981 | }, 982 | "autoload": { 983 | "classmap": [ 984 | "src/" 985 | ] 986 | }, 987 | "notification-url": "https://packagist.org/downloads/", 988 | "license": [ 989 | "BSD-3-Clause" 990 | ], 991 | "authors": [ 992 | { 993 | "name": "Sebastian Bergmann", 994 | "email": "sebastian@phpunit.de", 995 | "role": "lead" 996 | } 997 | ], 998 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 999 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 1000 | "keywords": [ 1001 | "filesystem", 1002 | "iterator" 1003 | ], 1004 | "funding": [ 1005 | { 1006 | "url": "https://github.com/sebastianbergmann", 1007 | "type": "github" 1008 | } 1009 | ], 1010 | "time": "2020-07-11T05:18:21+00:00" 1011 | }, 1012 | { 1013 | "name": "phpunit/php-invoker", 1014 | "version": "3.1.0", 1015 | "source": { 1016 | "type": "git", 1017 | "url": "https://github.com/sebastianbergmann/php-invoker.git", 1018 | "reference": "7a85b66acc48cacffdf87dadd3694e7123674298" 1019 | }, 1020 | "dist": { 1021 | "type": "zip", 1022 | "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/7a85b66acc48cacffdf87dadd3694e7123674298", 1023 | "reference": "7a85b66acc48cacffdf87dadd3694e7123674298", 1024 | "shasum": "" 1025 | }, 1026 | "require": { 1027 | "php": "^7.3 || ^8.0" 1028 | }, 1029 | "require-dev": { 1030 | "ext-pcntl": "*", 1031 | "phpunit/phpunit": "^9.0" 1032 | }, 1033 | "suggest": { 1034 | "ext-pcntl": "*" 1035 | }, 1036 | "type": "library", 1037 | "extra": { 1038 | "branch-alias": { 1039 | "dev-master": "3.1-dev" 1040 | } 1041 | }, 1042 | "autoload": { 1043 | "classmap": [ 1044 | "src/" 1045 | ] 1046 | }, 1047 | "notification-url": "https://packagist.org/downloads/", 1048 | "license": [ 1049 | "BSD-3-Clause" 1050 | ], 1051 | "authors": [ 1052 | { 1053 | "name": "Sebastian Bergmann", 1054 | "email": "sebastian@phpunit.de", 1055 | "role": "lead" 1056 | } 1057 | ], 1058 | "description": "Invoke callables with a timeout", 1059 | "homepage": "https://github.com/sebastianbergmann/php-invoker/", 1060 | "keywords": [ 1061 | "process" 1062 | ], 1063 | "funding": [ 1064 | { 1065 | "url": "https://github.com/sebastianbergmann", 1066 | "type": "github" 1067 | } 1068 | ], 1069 | "time": "2020-08-06T07:04:15+00:00" 1070 | }, 1071 | { 1072 | "name": "phpunit/php-text-template", 1073 | "version": "2.0.2", 1074 | "source": { 1075 | "type": "git", 1076 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 1077 | "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324" 1078 | }, 1079 | "dist": { 1080 | "type": "zip", 1081 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", 1082 | "reference": "6ff9c8ea4d3212b88fcf74e25e516e2c51c99324", 1083 | "shasum": "" 1084 | }, 1085 | "require": { 1086 | "php": "^7.3 || ^8.0" 1087 | }, 1088 | "require-dev": { 1089 | "phpunit/phpunit": "^9.0" 1090 | }, 1091 | "type": "library", 1092 | "extra": { 1093 | "branch-alias": { 1094 | "dev-master": "2.0-dev" 1095 | } 1096 | }, 1097 | "autoload": { 1098 | "classmap": [ 1099 | "src/" 1100 | ] 1101 | }, 1102 | "notification-url": "https://packagist.org/downloads/", 1103 | "license": [ 1104 | "BSD-3-Clause" 1105 | ], 1106 | "authors": [ 1107 | { 1108 | "name": "Sebastian Bergmann", 1109 | "email": "sebastian@phpunit.de", 1110 | "role": "lead" 1111 | } 1112 | ], 1113 | "description": "Simple template engine.", 1114 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 1115 | "keywords": [ 1116 | "template" 1117 | ], 1118 | "funding": [ 1119 | { 1120 | "url": "https://github.com/sebastianbergmann", 1121 | "type": "github" 1122 | } 1123 | ], 1124 | "time": "2020-06-26T11:55:37+00:00" 1125 | }, 1126 | { 1127 | "name": "phpunit/php-timer", 1128 | "version": "5.0.1", 1129 | "source": { 1130 | "type": "git", 1131 | "url": "https://github.com/sebastianbergmann/php-timer.git", 1132 | "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7" 1133 | }, 1134 | "dist": { 1135 | "type": "zip", 1136 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/cc49734779cbb302bf51a44297dab8c4bbf941e7", 1137 | "reference": "cc49734779cbb302bf51a44297dab8c4bbf941e7", 1138 | "shasum": "" 1139 | }, 1140 | "require": { 1141 | "php": "^7.3 || ^8.0" 1142 | }, 1143 | "require-dev": { 1144 | "phpunit/phpunit": "^9.2" 1145 | }, 1146 | "type": "library", 1147 | "extra": { 1148 | "branch-alias": { 1149 | "dev-master": "5.0-dev" 1150 | } 1151 | }, 1152 | "autoload": { 1153 | "classmap": [ 1154 | "src/" 1155 | ] 1156 | }, 1157 | "notification-url": "https://packagist.org/downloads/", 1158 | "license": [ 1159 | "BSD-3-Clause" 1160 | ], 1161 | "authors": [ 1162 | { 1163 | "name": "Sebastian Bergmann", 1164 | "email": "sebastian@phpunit.de", 1165 | "role": "lead" 1166 | } 1167 | ], 1168 | "description": "Utility class for timing", 1169 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 1170 | "keywords": [ 1171 | "timer" 1172 | ], 1173 | "funding": [ 1174 | { 1175 | "url": "https://github.com/sebastianbergmann", 1176 | "type": "github" 1177 | } 1178 | ], 1179 | "time": "2020-06-26T11:58:13+00:00" 1180 | }, 1181 | { 1182 | "name": "phpunit/phpunit", 1183 | "version": "9.3.10", 1184 | "source": { 1185 | "type": "git", 1186 | "url": "https://github.com/sebastianbergmann/phpunit.git", 1187 | "reference": "919333f2d046a89f9238f15d09f17a8f0baa5cc2" 1188 | }, 1189 | "dist": { 1190 | "type": "zip", 1191 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/919333f2d046a89f9238f15d09f17a8f0baa5cc2", 1192 | "reference": "919333f2d046a89f9238f15d09f17a8f0baa5cc2", 1193 | "shasum": "" 1194 | }, 1195 | "require": { 1196 | "doctrine/instantiator": "^1.3.1", 1197 | "ext-dom": "*", 1198 | "ext-json": "*", 1199 | "ext-libxml": "*", 1200 | "ext-mbstring": "*", 1201 | "ext-xml": "*", 1202 | "ext-xmlwriter": "*", 1203 | "myclabs/deep-copy": "^1.10.1", 1204 | "phar-io/manifest": "^2.0.1", 1205 | "phar-io/version": "^3.0.2", 1206 | "php": ">=7.3", 1207 | "phpspec/prophecy": "^1.11.1", 1208 | "phpunit/php-code-coverage": "^9.1.5", 1209 | "phpunit/php-file-iterator": "^3.0.4", 1210 | "phpunit/php-invoker": "^3.1", 1211 | "phpunit/php-text-template": "^2.0.2", 1212 | "phpunit/php-timer": "^5.0.1", 1213 | "sebastian/cli-parser": "^1.0", 1214 | "sebastian/code-unit": "^1.0.5", 1215 | "sebastian/comparator": "^4.0.3", 1216 | "sebastian/diff": "^4.0.2", 1217 | "sebastian/environment": "^5.1.2", 1218 | "sebastian/exporter": "^4.0.2", 1219 | "sebastian/global-state": "^5.0", 1220 | "sebastian/object-enumerator": "^4.0.2", 1221 | "sebastian/resource-operations": "^3.0.2", 1222 | "sebastian/type": "^2.2.1", 1223 | "sebastian/version": "^3.0.1" 1224 | }, 1225 | "require-dev": { 1226 | "ext-pdo": "*", 1227 | "phpspec/prophecy-phpunit": "^2.0.1" 1228 | }, 1229 | "suggest": { 1230 | "ext-soap": "*", 1231 | "ext-xdebug": "*" 1232 | }, 1233 | "bin": [ 1234 | "phpunit" 1235 | ], 1236 | "type": "library", 1237 | "extra": { 1238 | "branch-alias": { 1239 | "dev-master": "9.3-dev" 1240 | } 1241 | }, 1242 | "autoload": { 1243 | "files": [ 1244 | "src/Framework/Assert/Functions.php" 1245 | ], 1246 | "classmap": [ 1247 | "src/" 1248 | ] 1249 | }, 1250 | "notification-url": "https://packagist.org/downloads/", 1251 | "license": [ 1252 | "BSD-3-Clause" 1253 | ], 1254 | "authors": [ 1255 | { 1256 | "name": "Sebastian Bergmann", 1257 | "email": "sebastian@phpunit.de", 1258 | "role": "lead" 1259 | } 1260 | ], 1261 | "description": "The PHP Unit Testing framework.", 1262 | "homepage": "https://phpunit.de/", 1263 | "keywords": [ 1264 | "phpunit", 1265 | "testing", 1266 | "xunit" 1267 | ], 1268 | "funding": [ 1269 | { 1270 | "url": "https://phpunit.de/donate.html", 1271 | "type": "custom" 1272 | }, 1273 | { 1274 | "url": "https://github.com/sebastianbergmann", 1275 | "type": "github" 1276 | } 1277 | ], 1278 | "time": "2020-09-12T09:34:39+00:00" 1279 | }, 1280 | { 1281 | "name": "sebastian/cli-parser", 1282 | "version": "1.0.0", 1283 | "source": { 1284 | "type": "git", 1285 | "url": "https://github.com/sebastianbergmann/cli-parser.git", 1286 | "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82" 1287 | }, 1288 | "dist": { 1289 | "type": "zip", 1290 | "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", 1291 | "reference": "2a4a38c56e62f7295bedb8b1b7439ad523d4ea82", 1292 | "shasum": "" 1293 | }, 1294 | "require": { 1295 | "php": "^7.3 || ^8.0" 1296 | }, 1297 | "require-dev": { 1298 | "phpunit/phpunit": "^9.3" 1299 | }, 1300 | "type": "library", 1301 | "extra": { 1302 | "branch-alias": { 1303 | "dev-master": "1.0-dev" 1304 | } 1305 | }, 1306 | "autoload": { 1307 | "classmap": [ 1308 | "src/" 1309 | ] 1310 | }, 1311 | "notification-url": "https://packagist.org/downloads/", 1312 | "license": [ 1313 | "BSD-3-Clause" 1314 | ], 1315 | "authors": [ 1316 | { 1317 | "name": "Sebastian Bergmann", 1318 | "email": "sebastian@phpunit.de", 1319 | "role": "lead" 1320 | } 1321 | ], 1322 | "description": "Library for parsing CLI options", 1323 | "homepage": "https://github.com/sebastianbergmann/cli-parser", 1324 | "funding": [ 1325 | { 1326 | "url": "https://github.com/sebastianbergmann", 1327 | "type": "github" 1328 | } 1329 | ], 1330 | "time": "2020-08-12T10:49:21+00:00" 1331 | }, 1332 | { 1333 | "name": "sebastian/code-unit", 1334 | "version": "1.0.5", 1335 | "source": { 1336 | "type": "git", 1337 | "url": "https://github.com/sebastianbergmann/code-unit.git", 1338 | "reference": "c1e2df332c905079980b119c4db103117e5e5c90" 1339 | }, 1340 | "dist": { 1341 | "type": "zip", 1342 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/c1e2df332c905079980b119c4db103117e5e5c90", 1343 | "reference": "c1e2df332c905079980b119c4db103117e5e5c90", 1344 | "shasum": "" 1345 | }, 1346 | "require": { 1347 | "php": "^7.3 || ^8.0" 1348 | }, 1349 | "require-dev": { 1350 | "phpunit/phpunit": "^9.0" 1351 | }, 1352 | "type": "library", 1353 | "extra": { 1354 | "branch-alias": { 1355 | "dev-master": "1.0-dev" 1356 | } 1357 | }, 1358 | "autoload": { 1359 | "classmap": [ 1360 | "src/" 1361 | ] 1362 | }, 1363 | "notification-url": "https://packagist.org/downloads/", 1364 | "license": [ 1365 | "BSD-3-Clause" 1366 | ], 1367 | "authors": [ 1368 | { 1369 | "name": "Sebastian Bergmann", 1370 | "email": "sebastian@phpunit.de", 1371 | "role": "lead" 1372 | } 1373 | ], 1374 | "description": "Collection of value objects that represent the PHP code units", 1375 | "homepage": "https://github.com/sebastianbergmann/code-unit", 1376 | "funding": [ 1377 | { 1378 | "url": "https://github.com/sebastianbergmann", 1379 | "type": "github" 1380 | } 1381 | ], 1382 | "time": "2020-06-26T12:50:45+00:00" 1383 | }, 1384 | { 1385 | "name": "sebastian/code-unit-reverse-lookup", 1386 | "version": "2.0.2", 1387 | "source": { 1388 | "type": "git", 1389 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 1390 | "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819" 1391 | }, 1392 | "dist": { 1393 | "type": "zip", 1394 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ee51f9bb0c6d8a43337055db3120829fa14da819", 1395 | "reference": "ee51f9bb0c6d8a43337055db3120829fa14da819", 1396 | "shasum": "" 1397 | }, 1398 | "require": { 1399 | "php": "^7.3 || ^8.0" 1400 | }, 1401 | "require-dev": { 1402 | "phpunit/phpunit": "^9.0" 1403 | }, 1404 | "type": "library", 1405 | "extra": { 1406 | "branch-alias": { 1407 | "dev-master": "2.0-dev" 1408 | } 1409 | }, 1410 | "autoload": { 1411 | "classmap": [ 1412 | "src/" 1413 | ] 1414 | }, 1415 | "notification-url": "https://packagist.org/downloads/", 1416 | "license": [ 1417 | "BSD-3-Clause" 1418 | ], 1419 | "authors": [ 1420 | { 1421 | "name": "Sebastian Bergmann", 1422 | "email": "sebastian@phpunit.de" 1423 | } 1424 | ], 1425 | "description": "Looks up which function or method a line of code belongs to", 1426 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 1427 | "funding": [ 1428 | { 1429 | "url": "https://github.com/sebastianbergmann", 1430 | "type": "github" 1431 | } 1432 | ], 1433 | "time": "2020-06-26T12:04:00+00:00" 1434 | }, 1435 | { 1436 | "name": "sebastian/comparator", 1437 | "version": "4.0.3", 1438 | "source": { 1439 | "type": "git", 1440 | "url": "https://github.com/sebastianbergmann/comparator.git", 1441 | "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f" 1442 | }, 1443 | "dist": { 1444 | "type": "zip", 1445 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", 1446 | "reference": "dcc580eadfaa4e7f9d2cf9ae1922134ea962e14f", 1447 | "shasum": "" 1448 | }, 1449 | "require": { 1450 | "php": "^7.3 || ^8.0", 1451 | "sebastian/diff": "^4.0", 1452 | "sebastian/exporter": "^4.0" 1453 | }, 1454 | "require-dev": { 1455 | "phpunit/phpunit": "^9.0" 1456 | }, 1457 | "type": "library", 1458 | "extra": { 1459 | "branch-alias": { 1460 | "dev-master": "4.0-dev" 1461 | } 1462 | }, 1463 | "autoload": { 1464 | "classmap": [ 1465 | "src/" 1466 | ] 1467 | }, 1468 | "notification-url": "https://packagist.org/downloads/", 1469 | "license": [ 1470 | "BSD-3-Clause" 1471 | ], 1472 | "authors": [ 1473 | { 1474 | "name": "Sebastian Bergmann", 1475 | "email": "sebastian@phpunit.de" 1476 | }, 1477 | { 1478 | "name": "Jeff Welch", 1479 | "email": "whatthejeff@gmail.com" 1480 | }, 1481 | { 1482 | "name": "Volker Dusch", 1483 | "email": "github@wallbash.com" 1484 | }, 1485 | { 1486 | "name": "Bernhard Schussek", 1487 | "email": "bschussek@2bepublished.at" 1488 | } 1489 | ], 1490 | "description": "Provides the functionality to compare PHP values for equality", 1491 | "homepage": "https://github.com/sebastianbergmann/comparator", 1492 | "keywords": [ 1493 | "comparator", 1494 | "compare", 1495 | "equality" 1496 | ], 1497 | "funding": [ 1498 | { 1499 | "url": "https://github.com/sebastianbergmann", 1500 | "type": "github" 1501 | } 1502 | ], 1503 | "time": "2020-06-26T12:05:46+00:00" 1504 | }, 1505 | { 1506 | "name": "sebastian/complexity", 1507 | "version": "2.0.0", 1508 | "source": { 1509 | "type": "git", 1510 | "url": "https://github.com/sebastianbergmann/complexity.git", 1511 | "reference": "33fcd6a26656c6546f70871244ecba4b4dced097" 1512 | }, 1513 | "dist": { 1514 | "type": "zip", 1515 | "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/33fcd6a26656c6546f70871244ecba4b4dced097", 1516 | "reference": "33fcd6a26656c6546f70871244ecba4b4dced097", 1517 | "shasum": "" 1518 | }, 1519 | "require": { 1520 | "nikic/php-parser": "^4.7", 1521 | "php": "^7.3 || ^8.0" 1522 | }, 1523 | "require-dev": { 1524 | "phpunit/phpunit": "^9.2" 1525 | }, 1526 | "type": "library", 1527 | "extra": { 1528 | "branch-alias": { 1529 | "dev-master": "2.0-dev" 1530 | } 1531 | }, 1532 | "autoload": { 1533 | "classmap": [ 1534 | "src/" 1535 | ] 1536 | }, 1537 | "notification-url": "https://packagist.org/downloads/", 1538 | "license": [ 1539 | "BSD-3-Clause" 1540 | ], 1541 | "authors": [ 1542 | { 1543 | "name": "Sebastian Bergmann", 1544 | "email": "sebastian@phpunit.de", 1545 | "role": "lead" 1546 | } 1547 | ], 1548 | "description": "Library for calculating the complexity of PHP code units", 1549 | "homepage": "https://github.com/sebastianbergmann/complexity", 1550 | "funding": [ 1551 | { 1552 | "url": "https://github.com/sebastianbergmann", 1553 | "type": "github" 1554 | } 1555 | ], 1556 | "time": "2020-07-25T14:01:34+00:00" 1557 | }, 1558 | { 1559 | "name": "sebastian/diff", 1560 | "version": "4.0.2", 1561 | "source": { 1562 | "type": "git", 1563 | "url": "https://github.com/sebastianbergmann/diff.git", 1564 | "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113" 1565 | }, 1566 | "dist": { 1567 | "type": "zip", 1568 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", 1569 | "reference": "1e90b4cf905a7d06c420b1d2e9d11a4dc8a13113", 1570 | "shasum": "" 1571 | }, 1572 | "require": { 1573 | "php": "^7.3 || ^8.0" 1574 | }, 1575 | "require-dev": { 1576 | "phpunit/phpunit": "^9.0", 1577 | "symfony/process": "^4.2 || ^5" 1578 | }, 1579 | "type": "library", 1580 | "extra": { 1581 | "branch-alias": { 1582 | "dev-master": "4.0-dev" 1583 | } 1584 | }, 1585 | "autoload": { 1586 | "classmap": [ 1587 | "src/" 1588 | ] 1589 | }, 1590 | "notification-url": "https://packagist.org/downloads/", 1591 | "license": [ 1592 | "BSD-3-Clause" 1593 | ], 1594 | "authors": [ 1595 | { 1596 | "name": "Sebastian Bergmann", 1597 | "email": "sebastian@phpunit.de" 1598 | }, 1599 | { 1600 | "name": "Kore Nordmann", 1601 | "email": "mail@kore-nordmann.de" 1602 | } 1603 | ], 1604 | "description": "Diff implementation", 1605 | "homepage": "https://github.com/sebastianbergmann/diff", 1606 | "keywords": [ 1607 | "diff", 1608 | "udiff", 1609 | "unidiff", 1610 | "unified diff" 1611 | ], 1612 | "funding": [ 1613 | { 1614 | "url": "https://github.com/sebastianbergmann", 1615 | "type": "github" 1616 | } 1617 | ], 1618 | "time": "2020-06-30T04:46:02+00:00" 1619 | }, 1620 | { 1621 | "name": "sebastian/environment", 1622 | "version": "5.1.2", 1623 | "source": { 1624 | "type": "git", 1625 | "url": "https://github.com/sebastianbergmann/environment.git", 1626 | "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2" 1627 | }, 1628 | "dist": { 1629 | "type": "zip", 1630 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", 1631 | "reference": "0a757cab9d5b7ef49a619f1143e6c9c1bc0fe9d2", 1632 | "shasum": "" 1633 | }, 1634 | "require": { 1635 | "php": "^7.3 || ^8.0" 1636 | }, 1637 | "require-dev": { 1638 | "phpunit/phpunit": "^9.0" 1639 | }, 1640 | "suggest": { 1641 | "ext-posix": "*" 1642 | }, 1643 | "type": "library", 1644 | "extra": { 1645 | "branch-alias": { 1646 | "dev-master": "5.0-dev" 1647 | } 1648 | }, 1649 | "autoload": { 1650 | "classmap": [ 1651 | "src/" 1652 | ] 1653 | }, 1654 | "notification-url": "https://packagist.org/downloads/", 1655 | "license": [ 1656 | "BSD-3-Clause" 1657 | ], 1658 | "authors": [ 1659 | { 1660 | "name": "Sebastian Bergmann", 1661 | "email": "sebastian@phpunit.de" 1662 | } 1663 | ], 1664 | "description": "Provides functionality to handle HHVM/PHP environments", 1665 | "homepage": "http://www.github.com/sebastianbergmann/environment", 1666 | "keywords": [ 1667 | "Xdebug", 1668 | "environment", 1669 | "hhvm" 1670 | ], 1671 | "funding": [ 1672 | { 1673 | "url": "https://github.com/sebastianbergmann", 1674 | "type": "github" 1675 | } 1676 | ], 1677 | "time": "2020-06-26T12:07:24+00:00" 1678 | }, 1679 | { 1680 | "name": "sebastian/exporter", 1681 | "version": "4.0.2", 1682 | "source": { 1683 | "type": "git", 1684 | "url": "https://github.com/sebastianbergmann/exporter.git", 1685 | "reference": "571d721db4aec847a0e59690b954af33ebf9f023" 1686 | }, 1687 | "dist": { 1688 | "type": "zip", 1689 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/571d721db4aec847a0e59690b954af33ebf9f023", 1690 | "reference": "571d721db4aec847a0e59690b954af33ebf9f023", 1691 | "shasum": "" 1692 | }, 1693 | "require": { 1694 | "php": "^7.3 || ^8.0", 1695 | "sebastian/recursion-context": "^4.0" 1696 | }, 1697 | "require-dev": { 1698 | "ext-mbstring": "*", 1699 | "phpunit/phpunit": "^9.2" 1700 | }, 1701 | "type": "library", 1702 | "extra": { 1703 | "branch-alias": { 1704 | "dev-master": "4.0-dev" 1705 | } 1706 | }, 1707 | "autoload": { 1708 | "classmap": [ 1709 | "src/" 1710 | ] 1711 | }, 1712 | "notification-url": "https://packagist.org/downloads/", 1713 | "license": [ 1714 | "BSD-3-Clause" 1715 | ], 1716 | "authors": [ 1717 | { 1718 | "name": "Sebastian Bergmann", 1719 | "email": "sebastian@phpunit.de" 1720 | }, 1721 | { 1722 | "name": "Jeff Welch", 1723 | "email": "whatthejeff@gmail.com" 1724 | }, 1725 | { 1726 | "name": "Volker Dusch", 1727 | "email": "github@wallbash.com" 1728 | }, 1729 | { 1730 | "name": "Adam Harvey", 1731 | "email": "aharvey@php.net" 1732 | }, 1733 | { 1734 | "name": "Bernhard Schussek", 1735 | "email": "bschussek@gmail.com" 1736 | } 1737 | ], 1738 | "description": "Provides the functionality to export PHP variables for visualization", 1739 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1740 | "keywords": [ 1741 | "export", 1742 | "exporter" 1743 | ], 1744 | "funding": [ 1745 | { 1746 | "url": "https://github.com/sebastianbergmann", 1747 | "type": "github" 1748 | } 1749 | ], 1750 | "time": "2020-06-26T12:08:55+00:00" 1751 | }, 1752 | { 1753 | "name": "sebastian/global-state", 1754 | "version": "5.0.0", 1755 | "source": { 1756 | "type": "git", 1757 | "url": "https://github.com/sebastianbergmann/global-state.git", 1758 | "reference": "22ae663c951bdc39da96603edc3239ed3a299097" 1759 | }, 1760 | "dist": { 1761 | "type": "zip", 1762 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/22ae663c951bdc39da96603edc3239ed3a299097", 1763 | "reference": "22ae663c951bdc39da96603edc3239ed3a299097", 1764 | "shasum": "" 1765 | }, 1766 | "require": { 1767 | "php": "^7.3 || ^8.0", 1768 | "sebastian/object-reflector": "^2.0", 1769 | "sebastian/recursion-context": "^4.0" 1770 | }, 1771 | "require-dev": { 1772 | "ext-dom": "*", 1773 | "phpunit/phpunit": "^9.3" 1774 | }, 1775 | "suggest": { 1776 | "ext-uopz": "*" 1777 | }, 1778 | "type": "library", 1779 | "extra": { 1780 | "branch-alias": { 1781 | "dev-master": "5.0-dev" 1782 | } 1783 | }, 1784 | "autoload": { 1785 | "classmap": [ 1786 | "src/" 1787 | ] 1788 | }, 1789 | "notification-url": "https://packagist.org/downloads/", 1790 | "license": [ 1791 | "BSD-3-Clause" 1792 | ], 1793 | "authors": [ 1794 | { 1795 | "name": "Sebastian Bergmann", 1796 | "email": "sebastian@phpunit.de" 1797 | } 1798 | ], 1799 | "description": "Snapshotting of global state", 1800 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1801 | "keywords": [ 1802 | "global state" 1803 | ], 1804 | "funding": [ 1805 | { 1806 | "url": "https://github.com/sebastianbergmann", 1807 | "type": "github" 1808 | } 1809 | ], 1810 | "time": "2020-08-07T04:09:03+00:00" 1811 | }, 1812 | { 1813 | "name": "sebastian/lines-of-code", 1814 | "version": "1.0.0", 1815 | "source": { 1816 | "type": "git", 1817 | "url": "https://github.com/sebastianbergmann/lines-of-code.git", 1818 | "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5" 1819 | }, 1820 | "dist": { 1821 | "type": "zip", 1822 | "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e02bf626f404b5daec382a7b8a6a4456e49017e5", 1823 | "reference": "e02bf626f404b5daec382a7b8a6a4456e49017e5", 1824 | "shasum": "" 1825 | }, 1826 | "require": { 1827 | "nikic/php-parser": "^4.6", 1828 | "php": "^7.3 || ^8.0" 1829 | }, 1830 | "require-dev": { 1831 | "phpunit/phpunit": "^9.2" 1832 | }, 1833 | "type": "library", 1834 | "extra": { 1835 | "branch-alias": { 1836 | "dev-master": "1.0-dev" 1837 | } 1838 | }, 1839 | "autoload": { 1840 | "classmap": [ 1841 | "src/" 1842 | ] 1843 | }, 1844 | "notification-url": "https://packagist.org/downloads/", 1845 | "license": [ 1846 | "BSD-3-Clause" 1847 | ], 1848 | "authors": [ 1849 | { 1850 | "name": "Sebastian Bergmann", 1851 | "email": "sebastian@phpunit.de", 1852 | "role": "lead" 1853 | } 1854 | ], 1855 | "description": "Library for counting the lines of code in PHP source code", 1856 | "homepage": "https://github.com/sebastianbergmann/lines-of-code", 1857 | "funding": [ 1858 | { 1859 | "url": "https://github.com/sebastianbergmann", 1860 | "type": "github" 1861 | } 1862 | ], 1863 | "time": "2020-07-22T18:33:42+00:00" 1864 | }, 1865 | { 1866 | "name": "sebastian/object-enumerator", 1867 | "version": "4.0.2", 1868 | "source": { 1869 | "type": "git", 1870 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1871 | "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8" 1872 | }, 1873 | "dist": { 1874 | "type": "zip", 1875 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/074fed2d0a6d08e1677dd8ce9d32aecb384917b8", 1876 | "reference": "074fed2d0a6d08e1677dd8ce9d32aecb384917b8", 1877 | "shasum": "" 1878 | }, 1879 | "require": { 1880 | "php": "^7.3 || ^8.0", 1881 | "sebastian/object-reflector": "^2.0", 1882 | "sebastian/recursion-context": "^4.0" 1883 | }, 1884 | "require-dev": { 1885 | "phpunit/phpunit": "^9.0" 1886 | }, 1887 | "type": "library", 1888 | "extra": { 1889 | "branch-alias": { 1890 | "dev-master": "4.0-dev" 1891 | } 1892 | }, 1893 | "autoload": { 1894 | "classmap": [ 1895 | "src/" 1896 | ] 1897 | }, 1898 | "notification-url": "https://packagist.org/downloads/", 1899 | "license": [ 1900 | "BSD-3-Clause" 1901 | ], 1902 | "authors": [ 1903 | { 1904 | "name": "Sebastian Bergmann", 1905 | "email": "sebastian@phpunit.de" 1906 | } 1907 | ], 1908 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1909 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1910 | "funding": [ 1911 | { 1912 | "url": "https://github.com/sebastianbergmann", 1913 | "type": "github" 1914 | } 1915 | ], 1916 | "time": "2020-06-26T12:11:32+00:00" 1917 | }, 1918 | { 1919 | "name": "sebastian/object-reflector", 1920 | "version": "2.0.2", 1921 | "source": { 1922 | "type": "git", 1923 | "url": "https://github.com/sebastianbergmann/object-reflector.git", 1924 | "reference": "127a46f6b057441b201253526f81d5406d6c7840" 1925 | }, 1926 | "dist": { 1927 | "type": "zip", 1928 | "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/127a46f6b057441b201253526f81d5406d6c7840", 1929 | "reference": "127a46f6b057441b201253526f81d5406d6c7840", 1930 | "shasum": "" 1931 | }, 1932 | "require": { 1933 | "php": "^7.3 || ^8.0" 1934 | }, 1935 | "require-dev": { 1936 | "phpunit/phpunit": "^9.0" 1937 | }, 1938 | "type": "library", 1939 | "extra": { 1940 | "branch-alias": { 1941 | "dev-master": "2.0-dev" 1942 | } 1943 | }, 1944 | "autoload": { 1945 | "classmap": [ 1946 | "src/" 1947 | ] 1948 | }, 1949 | "notification-url": "https://packagist.org/downloads/", 1950 | "license": [ 1951 | "BSD-3-Clause" 1952 | ], 1953 | "authors": [ 1954 | { 1955 | "name": "Sebastian Bergmann", 1956 | "email": "sebastian@phpunit.de" 1957 | } 1958 | ], 1959 | "description": "Allows reflection of object attributes, including inherited and non-public ones", 1960 | "homepage": "https://github.com/sebastianbergmann/object-reflector/", 1961 | "funding": [ 1962 | { 1963 | "url": "https://github.com/sebastianbergmann", 1964 | "type": "github" 1965 | } 1966 | ], 1967 | "time": "2020-06-26T12:12:55+00:00" 1968 | }, 1969 | { 1970 | "name": "sebastian/recursion-context", 1971 | "version": "4.0.2", 1972 | "source": { 1973 | "type": "git", 1974 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1975 | "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63" 1976 | }, 1977 | "dist": { 1978 | "type": "zip", 1979 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/062231bf61d2b9448c4fa5a7643b5e1829c11d63", 1980 | "reference": "062231bf61d2b9448c4fa5a7643b5e1829c11d63", 1981 | "shasum": "" 1982 | }, 1983 | "require": { 1984 | "php": "^7.3 || ^8.0" 1985 | }, 1986 | "require-dev": { 1987 | "phpunit/phpunit": "^9.0" 1988 | }, 1989 | "type": "library", 1990 | "extra": { 1991 | "branch-alias": { 1992 | "dev-master": "4.0-dev" 1993 | } 1994 | }, 1995 | "autoload": { 1996 | "classmap": [ 1997 | "src/" 1998 | ] 1999 | }, 2000 | "notification-url": "https://packagist.org/downloads/", 2001 | "license": [ 2002 | "BSD-3-Clause" 2003 | ], 2004 | "authors": [ 2005 | { 2006 | "name": "Sebastian Bergmann", 2007 | "email": "sebastian@phpunit.de" 2008 | }, 2009 | { 2010 | "name": "Jeff Welch", 2011 | "email": "whatthejeff@gmail.com" 2012 | }, 2013 | { 2014 | "name": "Adam Harvey", 2015 | "email": "aharvey@php.net" 2016 | } 2017 | ], 2018 | "description": "Provides functionality to recursively process PHP variables", 2019 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 2020 | "funding": [ 2021 | { 2022 | "url": "https://github.com/sebastianbergmann", 2023 | "type": "github" 2024 | } 2025 | ], 2026 | "time": "2020-06-26T12:14:17+00:00" 2027 | }, 2028 | { 2029 | "name": "sebastian/resource-operations", 2030 | "version": "3.0.2", 2031 | "source": { 2032 | "type": "git", 2033 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 2034 | "reference": "0653718a5a629b065e91f774595267f8dc32e213" 2035 | }, 2036 | "dist": { 2037 | "type": "zip", 2038 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/0653718a5a629b065e91f774595267f8dc32e213", 2039 | "reference": "0653718a5a629b065e91f774595267f8dc32e213", 2040 | "shasum": "" 2041 | }, 2042 | "require": { 2043 | "php": "^7.3 || ^8.0" 2044 | }, 2045 | "require-dev": { 2046 | "phpunit/phpunit": "^9.0" 2047 | }, 2048 | "type": "library", 2049 | "extra": { 2050 | "branch-alias": { 2051 | "dev-master": "3.0-dev" 2052 | } 2053 | }, 2054 | "autoload": { 2055 | "classmap": [ 2056 | "src/" 2057 | ] 2058 | }, 2059 | "notification-url": "https://packagist.org/downloads/", 2060 | "license": [ 2061 | "BSD-3-Clause" 2062 | ], 2063 | "authors": [ 2064 | { 2065 | "name": "Sebastian Bergmann", 2066 | "email": "sebastian@phpunit.de" 2067 | } 2068 | ], 2069 | "description": "Provides a list of PHP built-in functions that operate on resources", 2070 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 2071 | "funding": [ 2072 | { 2073 | "url": "https://github.com/sebastianbergmann", 2074 | "type": "github" 2075 | } 2076 | ], 2077 | "time": "2020-06-26T12:16:22+00:00" 2078 | }, 2079 | { 2080 | "name": "sebastian/type", 2081 | "version": "2.2.1", 2082 | "source": { 2083 | "type": "git", 2084 | "url": "https://github.com/sebastianbergmann/type.git", 2085 | "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a" 2086 | }, 2087 | "dist": { 2088 | "type": "zip", 2089 | "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/86991e2b33446cd96e648c18bcdb1e95afb2c05a", 2090 | "reference": "86991e2b33446cd96e648c18bcdb1e95afb2c05a", 2091 | "shasum": "" 2092 | }, 2093 | "require": { 2094 | "php": "^7.3 || ^8.0" 2095 | }, 2096 | "require-dev": { 2097 | "phpunit/phpunit": "^9.2" 2098 | }, 2099 | "type": "library", 2100 | "extra": { 2101 | "branch-alias": { 2102 | "dev-master": "2.2-dev" 2103 | } 2104 | }, 2105 | "autoload": { 2106 | "classmap": [ 2107 | "src/" 2108 | ] 2109 | }, 2110 | "notification-url": "https://packagist.org/downloads/", 2111 | "license": [ 2112 | "BSD-3-Clause" 2113 | ], 2114 | "authors": [ 2115 | { 2116 | "name": "Sebastian Bergmann", 2117 | "email": "sebastian@phpunit.de", 2118 | "role": "lead" 2119 | } 2120 | ], 2121 | "description": "Collection of value objects that represent the types of the PHP type system", 2122 | "homepage": "https://github.com/sebastianbergmann/type", 2123 | "funding": [ 2124 | { 2125 | "url": "https://github.com/sebastianbergmann", 2126 | "type": "github" 2127 | } 2128 | ], 2129 | "time": "2020-07-05T08:31:53+00:00" 2130 | }, 2131 | { 2132 | "name": "sebastian/version", 2133 | "version": "3.0.1", 2134 | "source": { 2135 | "type": "git", 2136 | "url": "https://github.com/sebastianbergmann/version.git", 2137 | "reference": "626586115d0ed31cb71483be55beb759b5af5a3c" 2138 | }, 2139 | "dist": { 2140 | "type": "zip", 2141 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/626586115d0ed31cb71483be55beb759b5af5a3c", 2142 | "reference": "626586115d0ed31cb71483be55beb759b5af5a3c", 2143 | "shasum": "" 2144 | }, 2145 | "require": { 2146 | "php": "^7.3 || ^8.0" 2147 | }, 2148 | "type": "library", 2149 | "extra": { 2150 | "branch-alias": { 2151 | "dev-master": "3.0-dev" 2152 | } 2153 | }, 2154 | "autoload": { 2155 | "classmap": [ 2156 | "src/" 2157 | ] 2158 | }, 2159 | "notification-url": "https://packagist.org/downloads/", 2160 | "license": [ 2161 | "BSD-3-Clause" 2162 | ], 2163 | "authors": [ 2164 | { 2165 | "name": "Sebastian Bergmann", 2166 | "email": "sebastian@phpunit.de", 2167 | "role": "lead" 2168 | } 2169 | ], 2170 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 2171 | "homepage": "https://github.com/sebastianbergmann/version", 2172 | "funding": [ 2173 | { 2174 | "url": "https://github.com/sebastianbergmann", 2175 | "type": "github" 2176 | } 2177 | ], 2178 | "time": "2020-06-26T12:18:43+00:00" 2179 | }, 2180 | { 2181 | "name": "theseer/tokenizer", 2182 | "version": "1.2.0", 2183 | "source": { 2184 | "type": "git", 2185 | "url": "https://github.com/theseer/tokenizer.git", 2186 | "reference": "75a63c33a8577608444246075ea0af0d052e452a" 2187 | }, 2188 | "dist": { 2189 | "type": "zip", 2190 | "url": "https://api.github.com/repos/theseer/tokenizer/zipball/75a63c33a8577608444246075ea0af0d052e452a", 2191 | "reference": "75a63c33a8577608444246075ea0af0d052e452a", 2192 | "shasum": "" 2193 | }, 2194 | "require": { 2195 | "ext-dom": "*", 2196 | "ext-tokenizer": "*", 2197 | "ext-xmlwriter": "*", 2198 | "php": "^7.2 || ^8.0" 2199 | }, 2200 | "type": "library", 2201 | "autoload": { 2202 | "classmap": [ 2203 | "src/" 2204 | ] 2205 | }, 2206 | "notification-url": "https://packagist.org/downloads/", 2207 | "license": [ 2208 | "BSD-3-Clause" 2209 | ], 2210 | "authors": [ 2211 | { 2212 | "name": "Arne Blankerts", 2213 | "email": "arne@blankerts.de", 2214 | "role": "Developer" 2215 | } 2216 | ], 2217 | "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", 2218 | "funding": [ 2219 | { 2220 | "url": "https://github.com/theseer", 2221 | "type": "github" 2222 | } 2223 | ], 2224 | "time": "2020-07-12T23:59:07+00:00" 2225 | }, 2226 | { 2227 | "name": "webmozart/assert", 2228 | "version": "1.9.1", 2229 | "source": { 2230 | "type": "git", 2231 | "url": "https://github.com/webmozarts/assert.git", 2232 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389" 2233 | }, 2234 | "dist": { 2235 | "type": "zip", 2236 | "url": "https://api.github.com/repos/webmozarts/assert/zipball/bafc69caeb4d49c39fd0779086c03a3738cbb389", 2237 | "reference": "bafc69caeb4d49c39fd0779086c03a3738cbb389", 2238 | "shasum": "" 2239 | }, 2240 | "require": { 2241 | "php": "^5.3.3 || ^7.0 || ^8.0", 2242 | "symfony/polyfill-ctype": "^1.8" 2243 | }, 2244 | "conflict": { 2245 | "phpstan/phpstan": "<0.12.20", 2246 | "vimeo/psalm": "<3.9.1" 2247 | }, 2248 | "require-dev": { 2249 | "phpunit/phpunit": "^4.8.36 || ^7.5.13" 2250 | }, 2251 | "type": "library", 2252 | "autoload": { 2253 | "psr-4": { 2254 | "Webmozart\\Assert\\": "src/" 2255 | } 2256 | }, 2257 | "notification-url": "https://packagist.org/downloads/", 2258 | "license": [ 2259 | "MIT" 2260 | ], 2261 | "authors": [ 2262 | { 2263 | "name": "Bernhard Schussek", 2264 | "email": "bschussek@gmail.com" 2265 | } 2266 | ], 2267 | "description": "Assertions to validate method input/output with nice error messages.", 2268 | "keywords": [ 2269 | "assert", 2270 | "check", 2271 | "validate" 2272 | ], 2273 | "time": "2020-07-08T17:02:28+00:00" 2274 | } 2275 | ], 2276 | "aliases": [], 2277 | "minimum-stability": "stable", 2278 | "stability-flags": [], 2279 | "prefer-stable": false, 2280 | "prefer-lowest": false, 2281 | "platform": { 2282 | "php": ">=7.3" 2283 | }, 2284 | "platform-dev": [], 2285 | "plugin-api-version": "1.1.0" 2286 | } 2287 | -------------------------------------------------------------------------------- /docs/screenshots/aliases.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/aliases.png -------------------------------------------------------------------------------- /docs/screenshots/background-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/background-color.png -------------------------------------------------------------------------------- /docs/screenshots/hello-version-help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/hello-version-help.png -------------------------------------------------------------------------------- /docs/screenshots/interactive-args.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/interactive-args.gif -------------------------------------------------------------------------------- /docs/screenshots/repeat-args-missing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/repeat-args-missing.png -------------------------------------------------------------------------------- /docs/screenshots/repeat-help-message.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tarsana/command/be3f5e50a5bd6aabf39c1e3961c41eaeb475c36a/docs/screenshots/repeat-help-message.png -------------------------------------------------------------------------------- /examples/HelloWorld.php: -------------------------------------------------------------------------------- 1 | name('Hello World') 10 | ->version('1.0.0-alpha') 11 | ->description('Shows a "Hello World" message') 12 | ->options(['--formal']) 13 | ->describe('--formal', 'Use formal "Greetings" instead of "Hello"'); 14 | } 15 | 16 | protected function execute() 17 | { 18 | $greeting = $this->option('--formal') ? 'Greetings' : 'Hello'; 19 | $this->console->out('Your name: '); 20 | $name = $this->console->readLine(); 21 | $this->console->line("{$greeting} {$name}"); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/ListCommand.php: -------------------------------------------------------------------------------- 1 | name('List') 10 | ->version('1.0.0-alpha') 11 | ->description('Lists files and directories in the current directory.'); 12 | } 13 | 14 | protected function execute() 15 | { 16 | foreach($this->fs->find('*')->asArray() as $file) { 17 | $this->console->line($file->name()); 18 | } 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /examples/RenderCommand.php: -------------------------------------------------------------------------------- 1 | name('Render') 10 | ->version('1.0.0') 11 | ->description('Renders the hello template.') 12 | ->syntax('name: (string:You)') 13 | ->describe('name', 'Your name.') 14 | ->templatesPath(TEMPLATES_PATH); 15 | // this points to /tests/resources/templates 16 | } 17 | 18 | protected function execute() 19 | { 20 | $message = $this->template('hello') 21 | ->render([ 22 | 'name' => $this->args->name 23 | ]); 24 | 25 | $this->console->line($message); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /examples/RepeatCommand.php: -------------------------------------------------------------------------------- 1 | name('Repeat') 10 | ->version('1.0.0') 11 | ->description('Repeats a word a number of times') 12 | ->syntax('word: string, count: (number: 3)') 13 | ->options(['--upper']) 14 | ->describe('word', 'The word to repeat') 15 | ->describe('count', 'The number of times to repeat the word') 16 | ->describe('--upper', 'Converts the result to uppercase'); 17 | } 18 | 19 | protected function execute() 20 | { 21 | $result = str_repeat($this->args->word, $this->args->count); 22 | if ($this->option('--upper')) 23 | $result = strtoupper($result); 24 | $this->console->line($result); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /examples/tests/HelloWorldTest.php: -------------------------------------------------------------------------------- 1 | withStdin("Amine\n") 12 | ->command(new HelloWorld) 13 | ->prints("Your name:") 14 | ->prints("Hello Amine
"); 15 | } 16 | 17 | public function test_it_uses_formal_greeting() 18 | { 19 | $this->withStdin("Amine\n") 20 | ->command(new HelloWorld, ['--formal']) 21 | ->prints("Your name:") 22 | ->prints("Greetings Amine
"); 23 | } 24 | 25 | public function test_it_shows_hello_world_version() 26 | { 27 | $this->command(new HelloWorld, ['--version']) 28 | ->printsExactly("Hello World version 1.0.0-alpha
"); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /examples/tests/ListCommandTest.php: -------------------------------------------------------------------------------- 1 | havingFile('demo.txt', 'Some text here!') 12 | ->havingFile('doc.pdf') 13 | ->havingDir('src') 14 | ->command(new ListCommand) 15 | ->printsExactly('demo.txt
doc.pdf
src
'); 16 | } 17 | 18 | public function test_it_prints_nothing_when_no_files() 19 | { 20 | $this->command(new ListCommand) 21 | ->printsExactly(''); 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /examples/tests/RenderCommandTest.php: -------------------------------------------------------------------------------- 1 | command(new RenderCommand) 12 | ->printsExactly("Hello You
"); 13 | } 14 | 15 | public function test_it_renders_with_custom_name() 16 | { 17 | $this->command(new RenderCommand, ['Foo']) 18 | ->printsExactly("Hello Foo
"); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/tests/RepeatCommandTest.php: -------------------------------------------------------------------------------- 1 | command(new RepeatCommand, ['foo']) 12 | ->argsEqual((object) [ 13 | 'word' => 'foo', 14 | 'count' => 3 15 | ]) 16 | ->optionsEqual([ 17 | '--upper' => false 18 | ]) 19 | ->printsExactly("foofoofoo
"); 20 | } 21 | 22 | public function test_it_repeats_word_n_times() 23 | { 24 | $this->command(new RepeatCommand, ['bar', '5']) 25 | ->argsEqual((object) [ 26 | 'word' => 'bar', 27 | 'count' => 5 28 | ]) 29 | ->optionsEqual([ 30 | '--upper' => false 31 | ]) 32 | ->printsExactly("barbarbarbarbar
"); 33 | } 34 | 35 | public function test_it_repeats_word_n_times_uppercase() 36 | { 37 | $this->command(new RepeatCommand, ['bar', '5', '--upper']) 38 | ->argsEqual((object) [ 39 | 'word' => 'bar', 40 | 'count' => 5 41 | ]) 42 | ->optionsEqual([ 43 | '--upper' => true 44 | ]) 45 | ->printsExactly("BARBARBARBARBAR
"); 46 | } 47 | 48 | public function test_it_runs_interatively() 49 | { 50 | $this->withStdin("Yo\n\n\n") 51 | ->command(new RepeatCommand, ['-i']) 52 | ->argsEqual((object) [ 53 | 'word' => 'Yo', 54 | 'count' => 3 55 | ]) 56 | ->optionsEqual([ 57 | '--upper' => false 58 | ]) 59 | ->prints("YoYoYo
"); 60 | } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | src 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | ./examples/tests/ 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/Command.php: -------------------------------------------------------------------------------- 1 | action($action); 43 | return $command; 44 | } 45 | 46 | public function __construct() 47 | { 48 | $this->commands([]) 49 | ->name('Unknown') 50 | ->version('1.0.0') 51 | ->description('...') 52 | ->descriptions([]) 53 | ->options([]) 54 | ->console(new Console) 55 | ->fs(new Filesystem('.')) 56 | ->configPaths([]) 57 | ->setupSubCommands() 58 | ->init(); 59 | } 60 | 61 | /** 62 | * name getter and setter. 63 | * 64 | * @param string 65 | * @return mixed 66 | */ 67 | public function name(string $value = null) 68 | { 69 | if (null === $value) { 70 | return $this->name; 71 | } 72 | $this->name = $value; 73 | return $this; 74 | } 75 | 76 | /** 77 | * version getter and setter. 78 | * 79 | * @param string 80 | * @return mixed 81 | */ 82 | public function version(string $value = null) 83 | { 84 | if (null === $value) { 85 | return $this->version; 86 | } 87 | $this->version = $value; 88 | return $this; 89 | } 90 | 91 | /** 92 | * description getter and setter. 93 | * 94 | * @param string 95 | * @return mixed 96 | */ 97 | public function description(string $value = null) 98 | { 99 | if (null === $value) { 100 | return $this->description; 101 | } 102 | $this->description = $value; 103 | return $this; 104 | } 105 | 106 | /** 107 | * descriptions getter and setter. 108 | * 109 | * @param string 110 | * @return mixed 111 | */ 112 | public function descriptions(array $value = null) 113 | { 114 | if (null === $value) { 115 | return $this->descriptions; 116 | } 117 | $this->descriptions = $value; 118 | return $this; 119 | } 120 | 121 | /** 122 | * syntax getter and setter. 123 | * 124 | * @param string|null $syntax 125 | * @return Syntax|self 126 | */ 127 | public function syntax(string $syntax = null) 128 | { 129 | if (null === $syntax) 130 | return $this->syntax; 131 | 132 | $this->syntax = S::syntax()->parse("{{$syntax}| }"); 133 | return $this; 134 | } 135 | 136 | /** 137 | * options getter and setter. 138 | * 139 | * @param array 140 | * @return mixed 141 | */ 142 | public function options(array $options = null) 143 | { 144 | if (null === $options) { 145 | return $this->options; 146 | } 147 | 148 | $this->options = []; 149 | foreach($options as $option) 150 | $this->options[$option] = false; 151 | 152 | return $this; 153 | } 154 | 155 | /** 156 | * option getter. 157 | * 158 | * @param string 159 | * @return mixed 160 | */ 161 | public function option(string $name) 162 | { 163 | if (!array_key_exists($name, $this->options)) 164 | throw new \InvalidArgumentException("Unknown option '{$name}'"); 165 | return $this->options[$name]; 166 | } 167 | 168 | /** 169 | * args getter and setter. 170 | * 171 | * @param stdClass 172 | * @return mixed 173 | */ 174 | public function args(\stdClass $value = null) 175 | { 176 | if (null === $value) { 177 | return $this->args; 178 | } 179 | $this->args = $value; 180 | return $this; 181 | } 182 | 183 | /** 184 | * console getter and setter. 185 | * 186 | * @param ConsoleInterface 187 | * @return mixed 188 | */ 189 | public function console(ConsoleInterface $value = null) 190 | { 191 | if (null === $value) { 192 | return $this->console; 193 | } 194 | $this->console = $value; 195 | foreach ($this->commands as $name => $command) { 196 | $command->console = $value; 197 | } 198 | return $this; 199 | } 200 | 201 | /** 202 | * fs getter and setter. 203 | * 204 | * @param Tarsana\IO\Filesystem|string 205 | * @return mixed 206 | */ 207 | public function fs($value = null) 208 | { 209 | if (null === $value) { 210 | return $this->fs; 211 | } 212 | if (is_string($value)) 213 | $value = new Filesystem($value); 214 | $this->fs = $value; 215 | foreach ($this->commands as $name => $command) { 216 | $command->fs = $value; 217 | } 218 | return $this; 219 | } 220 | 221 | /** 222 | * templatesLoader getter and setter. 223 | * 224 | * @param Tarsana\Command\Interfaces\Template\TemplateLoaderInterface 225 | * @return mixed 226 | */ 227 | public function templatesLoader(TemplateLoaderInterface $value = null) 228 | { 229 | if (null === $value) { 230 | return $this->templatesLoader; 231 | } 232 | $this->templatesLoader = $value; 233 | foreach ($this->commands as $name => $command) { 234 | $command->templatesLoader = $value; 235 | } 236 | return $this; 237 | } 238 | 239 | public function templatesPath(string $path, string $cachePath = null) 240 | { 241 | $this->templatesLoader = new TemplateLoader($path, $cachePath); 242 | foreach ($this->commands as $name => $command) { 243 | $command->templatesLoader = $this->templatesLoader(); 244 | } 245 | return $this; 246 | } 247 | 248 | public function template(string $name) 249 | { 250 | if (null === $this->templatesLoader) 251 | throw new \Exception("Please initialize the templates loader before trying to load templates!"); 252 | return $this->templatesLoader->load($name); 253 | } 254 | 255 | public function configPaths(array $paths) 256 | { 257 | $configLoader = new ConfigLoader($this->fs); 258 | $this->config = $configLoader->load($paths); 259 | foreach ($this->commands as $name => $command) { 260 | $command->config = $this->config; 261 | } 262 | return $this; 263 | } 264 | 265 | public function config(string $path = null) 266 | { 267 | return $this->config->get($path); 268 | } 269 | 270 | /** 271 | * action getter and setter. 272 | * 273 | * @param callable 274 | * @return mixed 275 | */ 276 | public function action(callable $value = null) 277 | { 278 | if (null === $value) { 279 | return $this->action; 280 | } 281 | $this->action = $value; 282 | return $this; 283 | } 284 | 285 | /** 286 | * commands getter and setter. 287 | * 288 | * @param array 289 | * @return mixed 290 | */ 291 | public function commands(array $value = null) 292 | { 293 | if (null === $value) { 294 | return $this->commands; 295 | } 296 | $this->commands = []; 297 | foreach ($value as $name => $command) { 298 | $this->command($name, $command); 299 | } 300 | return $this; 301 | } 302 | 303 | public function command(string $name, Command $command = null) 304 | { 305 | if (null === $command) { 306 | if (!array_key_exists($name, $this->commands)) 307 | throw new \InvalidArgumentException("subcommand '{$name}' not found!"); 308 | return $this->commands[$name]; 309 | } 310 | $this->commands[$name] = $command; 311 | return $this; 312 | } 313 | 314 | public function hasCommand(string $name) : bool 315 | { 316 | return array_key_exists($name, $this->commands); 317 | } 318 | 319 | protected function setupSubCommands() 320 | { 321 | return $this->command('--help', new HelpCommand($this)) 322 | ->command('--version', new VersionCommand($this)) 323 | ->command('-i', new InteractiveCommand($this)); 324 | } 325 | 326 | public function describe(string $name, string $description = null) 327 | { 328 | if (null === $description) 329 | return array_key_exists($name, $this->descriptions) 330 | ? $this->descriptions[$name] : ''; 331 | if (substr($name, 0, 2) == '--' && array_key_exists($name, $this->options())) { 332 | $this->descriptions[$name] = $description; 333 | return $this; 334 | } 335 | try { 336 | $this->syntax->field($name); 337 | // throws exception if field is missing 338 | $this->descriptions[$name] = $description; 339 | return $this; 340 | } catch (\Exception $e) { 341 | throw new \InvalidArgumentException("Unknown field '{$name}'"); 342 | } 343 | } 344 | 345 | public function run(array $args = null, array $options = [], bool $rawArgs = true) 346 | { 347 | try { 348 | $this->clear(); 349 | 350 | if ($rawArgs) { 351 | if (null === $args) { 352 | $args = $GLOBALS['argv']; 353 | array_shift($args); 354 | } 355 | 356 | if (!empty($args) && array_key_exists($args[0], $this->commands)) { 357 | $name = $args[0]; 358 | array_shift($args); 359 | return $this->command($name)->run($args); 360 | } 361 | 362 | $this->parseArguments($args); 363 | } else { 364 | $this->args = (object) $args; 365 | foreach ($options as $name) { 366 | if (!array_key_exists($name, $this->options)) 367 | throw new \Exception("Unknown option '{$name}'"); 368 | $this->options[$name] = true; 369 | } 370 | } 371 | 372 | return $this->fire(); 373 | } catch (\Exception $e) { 374 | $this->handleError($e); 375 | } 376 | } 377 | 378 | protected function fire() 379 | { 380 | return (null === $this->action) 381 | ? $this->execute() 382 | : ($this->action)($this); 383 | } 384 | 385 | protected function clear() 386 | { 387 | $this->args = null; 388 | foreach($this->options as $name => $value) { 389 | $this->options[$name] = false; 390 | } 391 | } 392 | 393 | protected function parseArguments(array $args) 394 | { 395 | $arguments = []; 396 | foreach ($args as &$arg) { 397 | if (array_key_exists($arg, $this->options)) 398 | $this->options[$arg] = true; 399 | else 400 | $arguments[] = $arg; 401 | } 402 | if (null === $this->syntax) { 403 | $this->args = null; 404 | } else { 405 | $arguments = T::join($arguments, ' '); 406 | $this->args = $this->syntax->parse($arguments); 407 | } 408 | } 409 | 410 | protected function handleError(\Exception $e) { 411 | $output = (new ExceptionPrinter)->print($e); 412 | $this->console()->error($output); 413 | } 414 | 415 | protected function init() {} 416 | protected function execute() {} 417 | 418 | } 419 | -------------------------------------------------------------------------------- /src/Commands/HelpCommand.php: -------------------------------------------------------------------------------- 1 | name('Help') 17 | ->description('Shows the help message.'); 18 | $this->helper = SyntaxHelper::instance(); 19 | } 20 | 21 | protected function setupSubCommands() 22 | { 23 | return $this; 24 | } 25 | 26 | protected function execute() 27 | { 28 | $parent = $this->parent; 29 | 30 | $text = "{$parent->name} version {$parent->version}" 31 | . "

{$parent->description}

" 32 | . $this->syntaxHelp() 33 | . $this->optionsHelp() 34 | . $this->subCommandsHelp(); 35 | 36 | $this->console()->out($text); 37 | } 38 | 39 | protected function syntaxHelp() : string 40 | { 41 | $syntax = $this->parent->syntax(); 42 | $helper = $this->helper; 43 | $text = ''; 44 | 45 | if ($syntax) { 46 | $string = $helper->asString($syntax); 47 | $text .= "Syntax: [options] {$string}
" 48 | . "Arguments:
"; 49 | foreach ($syntax->fields() as $name => $s) { 50 | $text .= $this->fieldHelp($name, $s); 51 | } 52 | } 53 | 54 | return $text; 55 | } 56 | 57 | protected function optionsHelp() : string 58 | { 59 | $options = array_keys($this->parent->options()); 60 | $text = ''; 61 | if (!empty($options)) { 62 | $text .= 'Options:
'; 63 | foreach ($options as $name) { 64 | $description = $this->parent()->describe($name); 65 | $text .= "{$name} {$description}
"; 66 | } 67 | } 68 | 69 | return $text; 70 | } 71 | 72 | protected function subCommandsHelp() : string 73 | { 74 | $subCommands = $this->parent->commands(); 75 | $text = ''; 76 | if (!empty($subCommands)) { 77 | $text .= 'SubCommands:
'; 78 | foreach ($subCommands as $name => $cmd) { 79 | $text .= "{$name} {$cmd->description()}
"; 80 | } 81 | } 82 | 83 | return $text; 84 | } 85 | 86 | protected function fieldHelp( 87 | string $name, Syntax $s, string $prefix = '', int $level = 1 88 | ) : string 89 | { 90 | $tabs = str_repeat('', $level); 91 | $optional = ($s instanceof OptionalSyntax); 92 | if ($optional) 93 | $default = 'default: ' . json_encode($s->getDefault()); 94 | else 95 | $default = 'required'; 96 | $description = $this->parent()->describe($prefix.$name); 97 | $syntax = $this->helper->asString($s); 98 | $text = "{$tabs}{$name} {$syntax}" 99 | . " {$description} ({$default})
"; 100 | $level ++; 101 | $prefix .= $name . '.'; 102 | foreach ($this->helper->fields($s) as $field => $syntax) { 103 | $text .= $this->fieldHelp($field, $syntax, $prefix, $level); 104 | } 105 | 106 | return $text; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/Commands/InteractiveCommand.php: -------------------------------------------------------------------------------- 1 | 'enter', 15 | 127 => 'backspace', 16 | 65 => 'up', 17 | 66 => 'down', 18 | 67 => 'right', 19 | 68 => 'left', 20 | 9 => 'tab' 21 | ]; 22 | 23 | protected $helper; 24 | protected $confirmSyntax; 25 | 26 | protected function init() 27 | { 28 | $this->name('Interactive') 29 | ->description('Reads the command arguments and options interactively.'); 30 | $this->helper = SyntaxHelper::instance(); 31 | $this->confirmSyntax = S::optional(S::boolean(), false); 32 | } 33 | 34 | protected function setupSubCommands() 35 | { 36 | return $this; 37 | } 38 | 39 | protected function execute() 40 | { 41 | $parent = $this->parent; 42 | $syntax = $parent->syntax(); 43 | $this->console->out(''); 44 | 45 | if ($syntax) { 46 | $args = $this->read($syntax); 47 | $parent->args($args); 48 | } 49 | 50 | $options = array_keys($parent->options()); 51 | $chosen = []; 52 | foreach($options as $option) { 53 | $bool = $this->read($this->confirmSyntax, $option, true); 54 | $parent->options[$option] = $bool; 55 | if ($bool) { 56 | $chosen[] = $option; 57 | } 58 | } 59 | 60 | $options = implode(' ', $chosen) . ' '; 61 | $args = $syntax ? $syntax->dump($args) : ''; 62 | 63 | $this->console->out(''); 64 | $this->console->line("> {$options}{$args}
"); 65 | 66 | return $this->parent->fire(); 67 | } 68 | 69 | protected function read(Syntax $syntax, string $prefix = '', bool $display = false) 70 | { 71 | if ($display) { 72 | $this->display($syntax, $prefix); 73 | } 74 | 75 | $type = $this->helper->type($syntax); 76 | $result = null; 77 | switch ($type) { 78 | case 'object': 79 | $result = $this->readObject($syntax, $prefix); 80 | break; 81 | case 'array': 82 | $result = $this->readArray($syntax, $prefix); 83 | break; 84 | case 'optional': 85 | $result = $this->readOptional($syntax, $prefix); 86 | break; 87 | default: 88 | $result = $this->readSimple($syntax); 89 | break; 90 | } 91 | 92 | return $result; 93 | } 94 | 95 | protected function display(Syntax $syntax, string $name) 96 | { 97 | $text = $this->helper->asString($syntax); 98 | $default = ''; 99 | if ($syntax instanceof OptionalSyntax) { 100 | $default = '(default: ' . json_encode($syntax->getDefault()) . ')'; 101 | } 102 | $description = $this->parent->describe($name); 103 | $this->console->out( 104 | "{$name} {$text}" 105 | . " {$description} {$default}
" 106 | ); 107 | } 108 | 109 | protected function readObject(ObjectSyntax $syntax, string $prefix) 110 | { 111 | $result = []; 112 | if ($prefix != '') 113 | $prefix .= '.'; 114 | foreach ($syntax->fields() as $name => $s) { 115 | $fullname = $prefix . $name; 116 | $result[$name] = $this->read($s, $fullname, true); 117 | } 118 | return (object) $result; 119 | } 120 | 121 | protected function readArray(ArraySyntax $syntax, string $prefix) 122 | { 123 | $result = []; 124 | $repeat = true; 125 | while ($repeat) { 126 | $result[] = $this->read($syntax->syntax(), $prefix); 127 | $this->console->out("Add new item to {$prefix}?
"); 128 | $repeat = $this->readOptional($this->confirmSyntax, ''); 129 | $this->clearLines(3); 130 | } 131 | return $result; 132 | } 133 | 134 | protected function readOptional(OptionalSyntax $syntax, string $prefix) 135 | { 136 | $default = $syntax->syntax()->dump($syntax->getDefault()); 137 | $this->console->out("{$default}"); 138 | $n = ord($this->console->char()); 139 | $this->console->out(''); 140 | if (array_key_exists($n, static::KEYS) && static::KEYS[$n] == 'enter') 141 | return $syntax->getDefault(); 142 | return $this->read($syntax->syntax(), $prefix); 143 | } 144 | 145 | protected function readSimple(Syntax $syntax) 146 | { 147 | $this->console->out('> '); 148 | $done = false; 149 | $text = ''; 150 | $result = null; 151 | while (! $done) { 152 | $c = $this->readChar(); 153 | switch($c) { 154 | case 'enter': 155 | $done = true; 156 | break; 157 | case 'backspace': 158 | $text = substr($text, 0, -1); 159 | break; 160 | default: 161 | $text .= $c; 162 | break; 163 | } 164 | 165 | try { 166 | $result = $syntax->parse($text); 167 | $this->clearLines(1); 168 | $this->console->out("> {$text}"); 169 | } catch (\Exception $e) { 170 | $this->clearLines(1); 171 | $this->console->out("> {$text}"); 172 | $done = false; 173 | } 174 | } 175 | $this->console->out('
'); 176 | 177 | return $result; 178 | } 179 | 180 | protected function readChar() : string 181 | { 182 | $c = $this->console->char(); 183 | if (ctype_print($c)) 184 | return $c; 185 | $n = ord($c); 186 | if ( 187 | array_key_exists($n, static::KEYS) 188 | && in_array(static::KEYS[$n], ['enter', 'backspace']) 189 | ) { 190 | return static::KEYS[$n]; 191 | } 192 | return ''; 193 | } 194 | 195 | protected function clearLines(int $number) 196 | { 197 | $text = '' 198 | . str_repeat('', $number - 1) 199 | . ''; 200 | $this->console->out($text); 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/Commands/VersionCommand.php: -------------------------------------------------------------------------------- 1 | name('Version') 10 | ->description('Shows the version.'); 11 | } 12 | 13 | protected function setupSubCommands() 14 | { 15 | return $this; 16 | } 17 | 18 | protected function execute() 19 | { 20 | $command = $this->parent(); 21 | $this->console()->line("{$command->name()} version {$command->version()}"); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Config/Config.php: -------------------------------------------------------------------------------- 1 | data = $data; 18 | } 19 | 20 | /** 21 | * Gets a configuration value by path. 22 | */ 23 | public function get(string $path = null) 24 | { 25 | if (null === $path) 26 | return $this->data; 27 | $keys = explode('.', $path); 28 | $value = $this->data; 29 | foreach ($keys as $key) { 30 | if (!is_array($value) || !array_key_exists($key, $value)) 31 | throw new \Exception("Unable to find a configuration value with path '{$path}'"); 32 | $value = $value[$key]; 33 | } 34 | return $value; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Config/ConfigLoader.php: -------------------------------------------------------------------------------- 1 | JsonDecoder::class 16 | ]; 17 | 18 | protected $fs; 19 | 20 | public function __construct(FilesystemInterface $fs) 21 | { 22 | $this->fs = $fs; 23 | } 24 | 25 | public function load(array $paths) : ConfigInterface 26 | { 27 | if (empty($paths)) 28 | return new Config([]); 29 | $data = []; 30 | foreach ($paths as $path) { 31 | $data[] = $this->decode($path); 32 | } 33 | $data = call_user_func_array('array_replace_recursive', $data); 34 | return new Config($data); 35 | } 36 | 37 | protected function decode(string $path) : array { 38 | if (! $this->fs->isFile($path)) 39 | return []; 40 | $file = $this->fs->file($path); 41 | $ext = $file->extension(); 42 | if (! array_key_exists($ext, static::$decoders)) 43 | throw new \Exception("Unknown configuration file extension '{$ext}'"); 44 | $decoderClass = static::$decoders[$ext]; 45 | $decoder = new $decoderClass; 46 | return $decoder->decode($file->content()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Console/Console.php: -------------------------------------------------------------------------------- 1 | out = new Writer; 22 | $this->err = new Writer('php://stderr'); 23 | $this->in = new Reader; 24 | $this->outTransformer = new OutTransformer; 25 | $aliases = [ 26 | '' => '', 27 | '' => '', 28 | '' => '', 29 | '' => '', 30 | '' => '', 31 | '' => '', 32 | '' => '', 33 | '' => '', 34 | '' => ' ', 35 | '
' => PHP_EOL 36 | ]; 37 | foreach ($aliases as $name => $value) { 38 | $this->outTransformer()->alias($name, $value); 39 | } 40 | } 41 | 42 | public function stdin(ReaderInterface $in = null) 43 | { 44 | if (null === $in) 45 | return $this->in; 46 | $this->in = $in; 47 | return $this; 48 | } 49 | 50 | public function stdout(WriterInterface $out = null) 51 | { 52 | if (null === $out) 53 | return $this->out; 54 | $this->out = $out; 55 | return $this; 56 | } 57 | 58 | public function stderr(WriterInterface $err = null) 59 | { 60 | if (null === $err) 61 | return $this->err; 62 | $this->err = $err; 63 | return $this; 64 | } 65 | 66 | public function outTransformer(TransformerInterface $value = null) 67 | { 68 | if (null === $value) { 69 | return $this->outTransformer; 70 | } 71 | $this->outTransformer = $value; 72 | return $this; 73 | } 74 | 75 | public function out(string $text) : ConsoleInterface 76 | { 77 | $this->out->write($this->outTransformer()->transform($text)); 78 | return $this; 79 | } 80 | 81 | public function line(string $text) : ConsoleInterface 82 | { 83 | return $this->out($text . '
'); 84 | } 85 | 86 | public function error(string $text) : ConsoleInterface 87 | { 88 | $text = "{$text}
"; 89 | $this->err->write($this->outTransformer()->transform($text)); 90 | return $this; 91 | } 92 | 93 | public function read() : string 94 | { 95 | return $this->in->read(); 96 | } 97 | 98 | public function readLine() : string 99 | { 100 | return $this->in->readLine(); 101 | } 102 | 103 | public function char() : string 104 | { 105 | readline_callback_handler_install('', function() {}); 106 | $c = $this->in->read(1); 107 | readline_callback_handler_remove(); 108 | return $c; 109 | } 110 | 111 | public function alias(string $name, string $value) : ConsoleInterface 112 | { 113 | $this->outTransformer->alias($name, $value); 114 | return $this; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/Console/ExceptionPrinter.php: -------------------------------------------------------------------------------- 1 | printParseException($e); 23 | 24 | return "{$e}"; 25 | } 26 | 27 | /** 28 | * Converts a parse exception to a string. 29 | * 30 | * @param Tarsana\Syntax\Exceptions\ParseException $e 31 | * @return string 32 | */ 33 | public function printParseException(ParseException $e) : string 34 | { 35 | $syntax = $e->syntax(); 36 | $error = ''; 37 | if ($syntax instanceof ObjectSyntax) { 38 | $i = $e->extra(); 39 | if ($i['type'] == 'invalid-field') 40 | $error = "{$i['field']} is invalid!"; 41 | if ($i['type'] == 'missing-field') 42 | $error = "{$i['field']} is missing!"; 43 | if ($i['type'] == 'additional-items') { 44 | $items = implode($syntax->separator(), $i['items']); 45 | $error = "additional items {$items}"; 46 | } 47 | } 48 | $syntax = $this->printSyntax($e->syntax()); 49 | 50 | $output = "Failed to parse '{$e->input()}' as {$syntax}"; 51 | if ('' != $error) 52 | $output .= " {$error}"; 53 | 54 | $previous = $e->previous(); 55 | if ($previous) { 56 | $output .= '
' . $this->printParseException($previous); 57 | } 58 | 59 | return $output; 60 | } 61 | 62 | protected function printSyntax(Syntax $s, bool $short = false) : string 63 | { 64 | if ($s instanceof ObjectSyntax) { 65 | if ($short) return 'object'; 66 | return implode($s->separator(), array_keys($s->fields())); 67 | } 68 | if ($s instanceof ArraySyntax) { 69 | if ($short) return 'array'; 70 | return $this->printSyntax($s->syntax()).$s->separator().'...'; 71 | } 72 | return (string) $s; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Console/OutTransformer.php: -------------------------------------------------------------------------------- 1 | '$2A', 11 | 'down' => '$2B', 12 | 'right' => '$2C', 13 | 'left' => '$2D', 14 | 'nextLine' => '$2E', 15 | 'prevLine' => '$2F', 16 | 'column' => '$2G', 17 | 18 | 'clearBefore' => '1J', 19 | 'clearAfter' => 'J', 20 | 'clearLine' => '2K', 21 | 'clearAll' => '3J', 22 | 'clear' => '2J', 23 | 24 | 'save' => 's', 25 | 'load' => 'u', 26 | 27 | 'color' => '38;5;$2m', 28 | 'background' => '48;5;$2m', 29 | 'reset' => '0m', 30 | 'bold' => '1m', 31 | 'underline' => '4m' 32 | ]; 33 | 34 | protected $aliases = []; 35 | 36 | public function alias(string $name, string $value) { 37 | $this->aliases[$name] = $value; 38 | return $this; 39 | } 40 | 41 | public function transform(string $text) : string 42 | { 43 | foreach ($this->aliases as $name => $value) { 44 | $text = str_replace($name, $value, $text); 45 | } 46 | 47 | foreach (self::CONTROLS as $name => $value) { 48 | $text = preg_replace("/<{$name}(:([^>]*))?>/", self::CSI . $value, $text); 49 | } 50 | 51 | return $text; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Helpers/Decoders/JsonDecoder.php: -------------------------------------------------------------------------------- 1 | type($syntax); 28 | if ($type == 'optional') 29 | return $this->asString($syntax->syntax()); 30 | switch ($type) { 31 | case 'object': 32 | return implode( 33 | $syntax->separator(), 34 | array_keys($syntax->fields()) 35 | ); 36 | break; 37 | case 'array': 38 | $text = $this->asString($syntax->syntax()); 39 | return "{$text}{$syntax->separator()}..."; 40 | break; 41 | default: 42 | return $type; 43 | } 44 | } 45 | 46 | public function fields(Syntax $syntax) : array 47 | { 48 | $type = $this->type($syntax); 49 | switch ($type) { 50 | case 'object': 51 | return $syntax->fields(); 52 | case 'array': 53 | case 'optional': 54 | return $this->fields($syntax->syntax()); 55 | } 56 | return []; 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/Interfaces/Config/ConfigInterface.php: -------------------------------------------------------------------------------- 1 | parent($parent) 26 | ->console($parent->console()) 27 | ->fs($parent->fs) 28 | ->templatesLoader($parent->templatesLoader); 29 | $this->config = $parent->config; 30 | } 31 | 32 | /** 33 | * parent getter and setter. 34 | * 35 | * @param Tarsana\Command\Command|null 36 | * @return Tarsana\Command\Command 37 | */ 38 | public function parent(Command $value = null) 39 | { 40 | if (null === $value) { 41 | return $this->parent; 42 | } 43 | $this->parent = $value; 44 | return $this; 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /src/Template/TemplateLoader.php: -------------------------------------------------------------------------------- 1 | TwigLoader::class 23 | ]; 24 | 25 | /** 26 | * Filesystem of the templates. 27 | * 28 | * @var Tarsana\IO\Interfaces\Filesystem 29 | */ 30 | protected $fs; 31 | 32 | /** 33 | * Array of template loaders. 34 | * 35 | * @var array 36 | */ 37 | protected $loaders; 38 | 39 | public function __construct(string $templatesPath, string $cachePath = null) 40 | { 41 | $this->init($templatesPath, $cachePath); 42 | } 43 | 44 | /** 45 | * Initialize the loader. 46 | * 47 | * @param string $templatesPath 48 | * @param string $cachePath 49 | * @return self 50 | */ 51 | public function init(string $templatesPath, string $cachePath = null) : TemplateLoaderInterface 52 | { 53 | 54 | $this->fs = new Filesystem($templatesPath); 55 | $this->loaders = []; 56 | 57 | foreach (self::$providers as $ext => $provider) { 58 | $this->loaders[$ext] = new $provider(); 59 | $this->loaders[$ext]->init($templatesPath, $cachePath); 60 | } 61 | 62 | return $this; 63 | } 64 | 65 | /** 66 | * Load a template by name. The name is the relative 67 | * path of the template file from the templates folder 68 | * The name is given without extension; Exceptions are 69 | * thrown if no file with supported extension is found 70 | * or if many exists. 71 | * 72 | * @param string $name 73 | * @return Tarsana\Command\Interfaces\TemplateInterface 74 | * @throws Tarsana\Command\Exceptions\TemplateNotFound 75 | * @throws Tarsana\Command\Exceptions\TemplateNameConflict 76 | */ 77 | public function load (string $name) : TemplateInterface 78 | { 79 | $supportedExtensions = array_keys(self::$providers); 80 | 81 | $fsPathLength = strlen($this->fs->path()); 82 | 83 | $files = $this->fs 84 | ->find("{$name}.*") 85 | ->files() 86 | ->asArray(); 87 | 88 | $found = []; 89 | foreach ($files as $file) { 90 | $ext = $file->extension(); 91 | if (!in_array($ext, $supportedExtensions)) 92 | continue; 93 | $found[] = [ 94 | 'name' => substr($file->path(), $fsPathLength), 95 | 'extension' => $ext 96 | ]; 97 | } 98 | 99 | if (count($found) == 0) { 100 | throw new \InvalidArgumentException("Unable to find template with name '{$name}' on '{$this->fs->path()}'"); 101 | } 102 | 103 | if (count($found) > 1) { 104 | throw new \InvalidArgumentException("Mutiple templates found for the name '{$name}' on '{$this->fs->path()}'"); 105 | } 106 | 107 | return $this->loaders[$found[0]['extension']]->load($found[0]['name']); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Template/Twig/TwigLoader.php: -------------------------------------------------------------------------------- 1 | env = new \Twig_Environment($loader, [ 31 | 'cache' => $cachePath, 32 | ]); 33 | } else { 34 | $this->env = new \Twig_Environment($loader); 35 | } 36 | return $this; 37 | } 38 | 39 | /** 40 | * Loads a template. 41 | * 42 | * @param string $name 43 | * @return Tarsana\Command\Template\Twig\TwigTemplate 44 | */ 45 | public function load(string $name) : TemplateInterface 46 | { 47 | return new TwigTemplate($this->env->loadTemplate($name)); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/Template/Twig/TwigTemplate.php: -------------------------------------------------------------------------------- 1 | twig = $twig; 22 | $this->data = []; 23 | } 24 | 25 | /** 26 | * Binds data to the template. 27 | * 28 | * @param array $data 29 | * @return self 30 | */ 31 | public function bind (array $data) : TemplateInterface 32 | { 33 | $this->data = array_merge($this->data, $data); 34 | return $this; 35 | } 36 | 37 | /** 38 | * Renders the template. 39 | * 40 | * @return string 41 | */ 42 | public function render(array $data = null) : string 43 | { 44 | if (null !== $data) { 45 | $this->bind($data); 46 | } 47 | return $this->twig->render($this->data); 48 | } 49 | 50 | /** 51 | * Clears the data. 52 | * 53 | * @return self 54 | */ 55 | public function clear () : TemplateInterface 56 | { 57 | $this->data = []; 58 | return $this; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tester/CommandTestCase.php: -------------------------------------------------------------------------------- 1 | mkdir('.', 0777, true); 23 | $this->fs = new Filesystem('.', $adapter); 24 | } 25 | 26 | protected function command( 27 | Command $command, array $args = [], 28 | array $options = [], bool $rawArgs = true 29 | ) { 30 | $stdin = new Buffer; 31 | $stdin->write($this->stdin); 32 | $console = (new Console) 33 | ->stdin($stdin) 34 | ->stdout(new Buffer) 35 | ->stderr(new Buffer) 36 | ->outTransformer(new Transformer); 37 | 38 | $this->fs->dir($command->fs()->path(), true); 39 | 40 | $command->console($console) 41 | ->fs($this->fs) 42 | ->run($args, $options, $rawArgs); 43 | 44 | $this->cmd = $command; 45 | $this->stdout = $console->stdout()->read(); 46 | $this->stderr = $console->stderr()->read(); 47 | 48 | return $this; 49 | } 50 | 51 | protected function prints(string $text) 52 | { 53 | $this->assertTrue( 54 | false !== strpos($this->stdout, $text), 55 | "Failed asserting that '{$this->stdout}' Contains '{$text}'" 56 | ); 57 | return $this; 58 | } 59 | 60 | protected function printsExactly(string $text) 61 | { 62 | $this->assertEquals($text, $this->stdout); 63 | return $this; 64 | } 65 | 66 | protected function printsError(string $text) 67 | { 68 | $this->assertTrue( 69 | false !== strpos($this->stderr, $text), 70 | "Failed asserting that '{$this->stderr}' Contains '{$text}'" 71 | ); 72 | return $this; 73 | } 74 | 75 | protected function argsEqual($args) 76 | { 77 | $this->assertEquals($args, $this->cmd->args()); 78 | return $this; 79 | } 80 | 81 | protected function optionsEqual(array $options) 82 | { 83 | $this->assertEquals($options, $this->cmd->options()); 84 | return $this; 85 | } 86 | 87 | protected function withStdin(string $text) { 88 | $this->stdin = $text; 89 | return $this; 90 | } 91 | 92 | protected function havingFile(string $path, string $content = '') 93 | { 94 | $this->fs->file($path, true)->content($content); 95 | return $this; 96 | } 97 | 98 | protected function havingDir(string $path) 99 | { 100 | $this->fs->dir($path, true); 101 | return $this; 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /tester/Mocks/Transformer.php: -------------------------------------------------------------------------------- 1 | action(function($app) { 14 | $app->console()->line("Hey!"); 15 | }); 16 | 17 | $c->command('hey', $subCommand); 18 | 19 | $this->command($c, ['hey']) 20 | ->printsExactly("Hey!
"); 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /tests/Acceptance/HandlesFilesystemTest.php: -------------------------------------------------------------------------------- 1 | fs()->file('test.txt')->content(); 12 | $app->console()->line($content); 13 | }); 14 | 15 | $this->fs 16 | ->file('test.txt', true) 17 | ->content('Hello World!'); 18 | 19 | $this->command($c) 20 | ->prints('Hello World!'); 21 | } 22 | 23 | public function test_it_lists_files_on_directory() { 24 | $c = C::create(function($app) { 25 | $dir = $app->fs()->dir('files'); 26 | $files = $dir->fs()->find('*')->files()->asArray(); 27 | foreach ($files as $file) { 28 | $app->console()->line($file->name()); 29 | } 30 | }); 31 | 32 | $this->fs->dir('files', true); 33 | $this->fs->file('files/text.txt', true); 34 | $this->fs->file('files/music.mp3', true); 35 | $this->fs->file('files/photo.png', true); 36 | 37 | $this->command($c) 38 | ->printsExactly("text.txt
music.mp3
photo.png
"); 39 | } 40 | 41 | public function test_it_creates_files_and_directories() { 42 | $c = C::create() 43 | ->action(function($app) { 44 | $app->fs()->dir('files', true); 45 | $app->fs()->file('files/demo.txt', true) 46 | ->content('Yo!'); 47 | }); 48 | 49 | $this->assertFalse($this->fs->isDir('files')); 50 | $this->assertFalse($this->fs->isFile('files/demo.txt')); 51 | 52 | $this->command($c); 53 | 54 | $this->assertTrue($this->fs->isDir('files')); 55 | $this->assertTrue($this->fs->isFile('files/demo.txt')); 56 | $this->assertEquals('Yo!', $this->fs->file('files/demo.txt')->content()); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Acceptance/LoadsConfigurationTest.php: -------------------------------------------------------------------------------- 1 | configPaths(['/home/user/.config.json', 'config.json']); 12 | 13 | $name = $app->config('name'); 14 | $repoURL = $app->config('repo.url'); 15 | $app->console()->line("{$name}:{$repoURL}"); 16 | }); 17 | 18 | $this->fs 19 | ->file('/home/user/.config.json', true) 20 | ->content(json_encode(['name' => 'user'])); 21 | 22 | $this->fs 23 | ->file('config.json', true) 24 | ->content(json_encode(['repo' => ['url' => 'tarsana']])); 25 | 26 | $this->command($c) 27 | ->prints('user:tarsana
'); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Acceptance/ParsesArgumentsTest.php: -------------------------------------------------------------------------------- 1 | command(C::create()) 10 | ->argsEqual(null) 11 | ->optionsEqual([]); 12 | } 13 | 14 | public function test_args_options() { 15 | $c = C::create() 16 | ->syntax('name,age:(number:11)') 17 | ->options(['--vip','--help']); 18 | 19 | $this->command($c, ['Foo', '21', '--vip']) 20 | ->argsEqual((object) ['name' => 'Foo', 'age' => 21]) 21 | ->optionsEqual(['--vip' => true, '--help' => false]); 22 | 23 | $this->command($c, ['Bar', '--vip', '--help']) 24 | ->argsEqual((object) ['name' => 'Bar', 'age' => 11]) 25 | ->optionsEqual(['--vip' => true, '--help' => true]); 26 | 27 | $this->command($c, ['--vip']) 28 | ->printsError("Failed to parse '' as name age name is missing!") 29 | ->printsError("Failed to parse '' as String"); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /tests/Acceptance/PrintsToConsoleTest.php: -------------------------------------------------------------------------------- 1 | command( 10 | C::create(function($app) { 11 | $app->console()->line("Hello World"); 12 | }) 13 | ) 14 | ->prints("Hello") 15 | ->prints("World") 16 | ->printsExactly("Hello World
"); 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /tests/Acceptance/ReadsArgumentsInteractivelyTest.php: -------------------------------------------------------------------------------- 1 | command(C::create(), ['-i']) 10 | ->argsEqual(null) 11 | ->optionsEqual([]); 12 | } 13 | 14 | public function test_args_options() { 15 | $c = C::create() 16 | ->syntax('name, age:(number:11), friends: [string]') 17 | ->options(['--vip','--help']); 18 | 19 | $this->withStdin("Foo\n\nBar\n y\nBaz\n n\n y\n\n") 20 | ->command($c, ['-i']) 21 | ->argsEqual((object) [ 22 | 'name' => 'Foo', 23 | 'age' => 11, 24 | 'friends' => ['Bar', 'Baz'] 25 | ]) 26 | ->optionsEqual(['--vip' => true, '--help' => false]); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /tests/Acceptance/RendersTemplatesTest.php: -------------------------------------------------------------------------------- 1 | templatesPath(TEMPLATES_PATH); 13 | $template = $app->template('hello'); 14 | $output = $template->render(['name' => 'You']); 15 | $app->console()->line($output); 16 | $output = $template 17 | ->clear() 18 | ->bind(['name' => 'Me']) 19 | ->render(); 20 | $app->console()->line($output); 21 | }); 22 | 23 | $this->command($c) 24 | ->printsExactly("Hello You
Hello Me
"); 25 | 26 | $c->templatesLoader(new TwigLoader(TEMPLATES_PATH, CACHE_PATH)); 27 | $this->command($c) 28 | ->printsExactly("Hello You
Hello Me
"); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /tests/Acceptance/ShowsHelpTest.php: -------------------------------------------------------------------------------- 1 | name('Class Generator') 12 | ->version('1.0.1') 13 | ->description('Generates basic code for a class.') 14 | ->syntax(' 15 | name, 16 | parents: ([string]:[]), 17 | interfaces: ([string]:[]), 18 | attrs: [{ 19 | name, 20 | type, 21 | hasGetter: (boolean:true), 22 | hasSetter: (boolean:true), 23 | isStatic: (boolean:false) 24 | }], 25 | methods: ([{ 26 | name: string, 27 | type: string, 28 | args: [{ name, type, default: (string:null) |.}], 29 | isStatic: (boolean:false) 30 | }]:[]) 31 | ') 32 | ->describe('name', 'The name of the class.') 33 | ->describe('parents', 'List of parent classes names.') 34 | ->describe('interfaces', 'List of implemented interfaces.') 35 | ->describe('attrs', 'List of attributes of the class.') 36 | ->describe('attrs.name', 'The name of the attribute.') 37 | ->describe('attrs.type', 'The type of the attribute.') 38 | ->describe('attrs.hasGetter', 'Generates a getter for the attribute.') 39 | ->describe('attrs.hasSetter', 'Generates a setter for the attribute.') 40 | ->describe('attrs.isStatic', 'The attribute is static.') 41 | ->describe('methods', 'List of methods of the class.') 42 | ->describe('methods.name', 'The method name.') 43 | ->describe('methods.type', 'The method return type.') 44 | ->describe('methods.args', 'List of arguments of the method.') 45 | ->describe('methods.isStatic', 'This method is static.'); 46 | 47 | $this->command($c, ['--help']) 48 | ->printsExactly( 49 | "Class Generator version 1.0.1
" 50 | . "
Generates basic code for a class.
" 51 | . "
Syntax: [options] name parents interfaces attrs methods
" 52 | . "Arguments:
" 53 | . "name string The name of the class. (required)
" 54 | . "parents string,... List of parent classes names. (default: [])
" 55 | . "interfaces string,... List of implemented interfaces. (default: [])" 56 | . "
attrs name:type:hasGetter:hasSetter:isStatic,... List of attributes of the class. (required)
" 57 | . "name string The name of the attribute. (required)
" 58 | . "type string The type of the attribute. (required)
" 59 | . "hasGetter boolean Generates a getter for the attribute. (default: true)
" 60 | . "hasSetter boolean Generates a setter for the attribute. (default: true)
" 61 | . "isStatic boolean The attribute is static. (default: false)
" 62 | . "methods name:type:args:isStatic,... List of methods of the class. (default: [])
" 63 | . "name string The method name. (required)
" 64 | . "type string The method return type. (required)
" 65 | . "args name.type.default,... List of arguments of the method. (required)
" 66 | . "name string (required)
" 67 | . "type string (required)
" 68 | . "default string (default: \"null\")
" 69 | . "isStatic boolean This method is static. (default: false)
" 70 | . "SubCommands:
" 71 | . "--help Shows the help message.
" 72 | . "--version Shows the version.
" 73 | . "-i Reads the command arguments and options interactively.
" 74 | ); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /tests/Acceptance/ShowsVersionTest.php: -------------------------------------------------------------------------------- 1 | name('Foo') 12 | ->version('2.0.1-beta.1.0.12'); 13 | $this->command($c, ['--version']) 14 | ->printsExactly("Foo version 2.0.1-beta.1.0.12
"); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/Unit/Config/ConfigLoaderTest.php: -------------------------------------------------------------------------------- 1 | mkdir('.', 0777, true); 18 | $this->fs = new Filesystem('.', $adapter); 19 | $this->loader = new ConfigLoader($this->fs); 20 | } 21 | 22 | public function test_it_loads_single_config() { 23 | $data = ['name' => 'foo', 'repo' => 'bar']; 24 | $this->fs->file('config.json', true) 25 | ->content(json_encode($data)); 26 | $this->assertEquals($data, $this->loader->load(['config.json'])->get()); 27 | } 28 | 29 | public function test_it_loads_many_configs() { 30 | $data1 = ['name' => 'foo', 'repo' => 'bar']; 31 | $data2 = ['repo' => ['type' => 'git']]; 32 | $data3 = ['repo' => ['name' => 'baz'], 'descr' => 'blabla']; 33 | $merged = ['name' => 'foo', 'repo' => ['type' => 'git', 'name' => 'baz'], 'descr' => 'blabla']; 34 | 35 | $this->fs->file('/opt/command/config.json', true)->content(json_encode($data1)); 36 | $this->fs->file('/home/user/config.json', true)->content(json_encode($data2)); 37 | $this->fs->file('config.json', true)->content(json_encode($data3)); 38 | 39 | $this->assertEquals($merged, $this->loader->load([ 40 | '/opt/command/config.json', 41 | '/home/user/config.json', 42 | '/projects/config.json', // this is missing 43 | 'config.json' 44 | ])->get()); 45 | } 46 | 47 | public function test_it_loads_empty_config_when_no_path_is_given() { 48 | $this->assertEquals([], $this->loader->load([])->get()); 49 | } 50 | 51 | public function test_it_throws_exception_when_unknown_extension() { 52 | $this->expectException('Exception'); 53 | $this->fs->file('config.xml', true); 54 | $this->loader->load(['config.xml']); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /tests/Unit/Config/ConfigTest.php: -------------------------------------------------------------------------------- 1 | 'Foo', 12 | 'urls' => [ 13 | 'github' => 'some-link-here' 14 | ] 15 | ]; 16 | 17 | $c = new Config($data); 18 | $this->assertEquals($data, $c->get()); 19 | $this->assertEquals('Foo', $c->get('name')); 20 | $this->assertEquals('some-link-here', $c->get('urls.github')); 21 | } 22 | 23 | public function test_it_throws_exception() { 24 | $this->expectException('Exception'); 25 | $data = [ 26 | 'name' => 'Foo', 27 | 'urls' => [ 28 | 'github' => 'some-link-here' 29 | ] 30 | ]; 31 | $c = new Config($data); 32 | $c->get('bar'); 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /tests/Unit/Console/ExceptionPrinterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals("{$e}", $p->print($e)); 15 | } 16 | 17 | public function test_it_prints_simple_parse_exception() { 18 | $p = new ExceptionPrinter; 19 | $e = new ParseException(S::number(), 'test', 0, 'Not a number!'); 20 | $this->assertEquals( 21 | "Failed to parse 'test' as Number", 22 | $p->print($e) 23 | ); 24 | } 25 | 26 | public function test_it_prints_array_parse_exception() { 27 | $p = new ExceptionPrinter; 28 | $syntax = S::array(S::number()); 29 | try { 30 | $syntax->parse('11,foo'); 31 | $this->assertTrue(false, "Exception not thrown!"); 32 | } catch(ParseException $e) { 33 | $this->assertEquals( 34 | "Failed to parse '11,foo' as Number,...
". 35 | "Failed to parse 'foo' as Number", 36 | $p->print($e) 37 | ); 38 | } 39 | } 40 | 41 | public function test_it_prints_object_parse_exception_field_is_missing() { 42 | $p = new ExceptionPrinter; 43 | $syntax = S::object([ 44 | 'name' => S::string(), 45 | 'age' => S::number() 46 | ]); 47 | try { 48 | $syntax->parse('foo'); 49 | $this->assertTrue(false, "Exception not thrown!"); 50 | } catch(ParseException $e) { 51 | $this->assertEquals( 52 | "Failed to parse 'foo' as name:age ". 53 | "age is missing!
". 54 | "Failed to parse '' as Number", 55 | $p->print($e) 56 | ); 57 | } 58 | } 59 | 60 | public function test_it_prints_object_parse_exception_field_is_invalid() { 61 | $p = new ExceptionPrinter; 62 | $syntax = S::object([ 63 | 'name' => S::string(), 64 | 'age' => S::number() 65 | ]); 66 | try { 67 | $syntax->parse('foo:bar'); 68 | $this->assertTrue(false, "Exception not thrown!"); 69 | } catch(ParseException $e) { 70 | $this->assertEquals( 71 | "Failed to parse 'foo:bar' as name:age ". 72 | "age is invalid!
". 73 | "Failed to parse 'bar' as Number", 74 | $p->print($e) 75 | ); 76 | } 77 | } 78 | 79 | public function test_it_prints_object_parse_exception_additional_items() { 80 | $p = new ExceptionPrinter; 81 | $syntax = S::object([ 82 | 'name' => S::string(), 83 | 'age' => S::number() 84 | ]); 85 | try { 86 | $syntax->parse('foo:11:baz:lorem'); 87 | $this->assertTrue(false, "Exception not thrown!"); 88 | } catch(ParseException $e) { 89 | $this->assertEquals( 90 | "Failed to parse 'foo:11:baz:lorem' as name:age ". 91 | "additional items baz:lorem", 92 | $p->print($e) 93 | ); 94 | } 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /tests/Unit/Console/OutTransformerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals( 12 | "\033[38;5;42m", 13 | $t->transform('') 14 | ); 15 | $this->assertEquals( 16 | "\033[48;5;42m", 17 | $t->transform('') 18 | ); 19 | } 20 | 21 | public function test_it_applies_aliases() { 22 | $t = new OutTransformer; 23 | $this->assertEquals("Line1
Line2", $t->transform('Line1
Line2')); 24 | 25 | $t->alias('
', PHP_EOL); 26 | $this->assertEquals("Line1".PHP_EOL."Line2", $t->transform('Line1
Line2')); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |