From 430a2010a8b9ddfed647ea92c6cb4ef996ba8b89 Mon Sep 17 00:00:00 2001 From: phpdevcommunity Date: Mon, 23 Dec 2024 07:29:58 +0000 Subject: [PATCH] initial commit for PHP Console v1 --- .gitignore | 2 + LICENSE | 21 ++ README.md | 477 +++++++++++++++++++++++++++++++ bin/console | 25 ++ composer.json | 27 ++ examples/commands.php | 38 +++ examples/output.php | 85 ++++++ src/Argument/CommandArgument.php | 50 ++++ src/Command/CommandInterface.php | 50 ++++ src/Command/HelpCommand.php | 66 +++++ src/CommandParser.php | 100 +++++++ src/CommandRunner.php | 155 ++++++++++ src/Input.php | 75 +++++ src/InputInterface.php | 14 + src/Option/CommandOption.php | 43 +++ src/Output.php | 36 +++ src/Output/ConsoleOutput.php | 342 ++++++++++++++++++++++ src/OutputInterface.php | 9 + tests/Command/FooCommand.php | 45 +++ tests/CommandArgumentTest.php | 93 ++++++ tests/CommandOptionTest.php | 65 +++++ tests/CommandParserTest.php | 81 ++++++ tests/CommandTest.php | 96 +++++++ tests/InputTest.php | 87 ++++++ tests/OutputTest.php | 51 ++++ 25 files changed, 2133 insertions(+) create mode 100644 .gitignore create mode 100755 LICENSE create mode 100644 README.md create mode 100644 bin/console create mode 100644 composer.json create mode 100755 examples/commands.php create mode 100644 examples/output.php create mode 100755 src/Argument/CommandArgument.php create mode 100644 src/Command/CommandInterface.php create mode 100644 src/Command/HelpCommand.php create mode 100755 src/CommandParser.php create mode 100644 src/CommandRunner.php create mode 100644 src/Input.php create mode 100644 src/InputInterface.php create mode 100755 src/Option/CommandOption.php create mode 100644 src/Output.php create mode 100755 src/Output/ConsoleOutput.php create mode 100644 src/OutputInterface.php create mode 100755 tests/Command/FooCommand.php create mode 100755 tests/CommandArgumentTest.php create mode 100755 tests/CommandOptionTest.php create mode 100755 tests/CommandParserTest.php create mode 100755 tests/CommandTest.php create mode 100755 tests/InputTest.php create mode 100755 tests/OutputTest.php diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f45219c --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor/ +/.idea \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100755 index 0000000..bb11db8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024 PhpDevCommunity + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..cc2194d --- /dev/null +++ b/README.md @@ -0,0 +1,477 @@ +# PHP Console + +A lightweight PHP library designed to simplify command handling in console applications. This library is dependency-free and focused on providing a streamlined and efficient solution for building PHP CLI tools. +## Installation + +You can install this library via [Composer](https://getcomposer.org/). Ensure your project meets the minimum PHP version requirement of 7.4. + +```bash +composer require phpdevcommunity/php-console +``` +## Requirements + +- PHP version 7.4 or higher + +## Table of Contents + +1. [Setup in a PHP Application](#setup-in-a-php-application) +2. [Creating a Command](#creating-a-command) +3. [Defining Arguments and Options](#defining-arguments-and-options) +4. [Handling Different Output Types](#handling-different-output-types) + + +I attempted to rewrite the chapter "Setup in a PHP Application" in English while updating the document, but the update failed due to an issue with the pattern-matching process. Let me fix the issue manually. Here's the rewritten content in English: + +--- + +## Setup in a PHP Application + +To use this library in any PHP application (Symfony, Laravel, Slim, or others), first create a `bin` directory in your project. Then, add a script file, for example, `bin/console`, with the following content: + +```php +#!/usr/bin/php +boot(); +// $container = $kernel->getContainer(); +// $app = $container->get(CommandRunner::class); + + +$app = new CommandRunner([]); +$exitCode = $app->run(new CommandParser(), new Output()); +exit($exitCode); +``` + +By convention, the file is named `bin/console`, but you can choose any name you prefer. This script serves as the main entry point for your CLI commands. Make sure the file is executable by running the following command: + +```bash +chmod +x bin/console +``` + +## Creating a Command + +To add a command to your application, you need to create a class that implements the `CommandInterface` interface. Here is an example implementation for a command called `send-email`: + +```php +use PhpDevCommunity\Console\Argument\CommandArgument; +use PhpDevCommunity\Console\Command\CommandInterface; +use PhpDevCommunity\Console\InputInterface; +use PhpDevCommunity\Console\Option\CommandOption; +use PhpDevCommunity\Console\OutputInterface; + +class SendEmailCommand implements CommandInterface +{ + public function getName(): string + { + return 'send-email'; + } + + public function getDescription(): string + { + return 'Sends an email to the specified recipient with an optional subject.'; + } + + public function getOptions(): array + { + return [ + new CommandOption('subject', 's', 'The subject of the email', false), + ]; + } + + public function getArguments(): array + { + return [ + new CommandArgument('recipient', true, null, 'The email address of the recipient'), + ]; + } + + public function execute(InputInterface $input, OutputInterface $output): void + { + // Validate and retrieve the recipient email + if (!$input->hasArgument('recipient')) { + $output->writeln('Error: The recipient email is required.'); + return; + } + $recipient = $input->getArgumentValue('recipient'); + + // Validate email format + if (!filter_var($recipient, FILTER_VALIDATE_EMAIL)) { + $output->writeln('Error: The provided email address is not valid.'); + return; + } + + // Retrieve the subject option (if provided) + $subject = $input->hasOption('subject') ? $input->getOptionValue('subject') : 'No subject'; + + // Simulate email sending + $output->writeln('Sending email...'); + $output->writeln('Recipient: ' . $recipient); + $output->writeln('Subject: ' . $subject); + $output->writeln('Email sent successfully!'); + } +} +``` + +### Registering the Command in `CommandRunner` + +After creating your command, you need to register it in the `CommandRunner` so that it can be executed. Here is an example of how to register the `SendEmailCommand`: + +```php +use PhpDevCommunity\Console\CommandRunner; + +$app = new CommandRunner([ + new SendEmailCommand() +]); + +``` + +The `CommandRunner` takes an array of commands as its parameter. Each command should be an instance of a class that implements the `CommandInterface`. Once registered, the command can be called from the console. + +### Example Usage in the Terminal + +1. **Command without a subject option:** + + ```bash + bin/console send-email john.doe@example.com + ``` + + **Output:** + ``` + Sending email... + Recipient: john.doe@example.com + Subject: No subject + Email sent successfully! + ``` + +2. **Command with a subject option:** + + ```bash + bin/console send-email john.doe@example.com --subject "Meeting Reminder" + ``` + + **Output:** + ``` + Sending email... + Recipient: john.doe@example.com + Subject: Meeting Reminder + Email sent successfully! + ``` + +3. **Command with an invalid email format:** + + ```bash + bin/console send-email invalid-email + ``` + + **Output:** + ``` + Error: The provided email address is not valid. + ``` + +4. **Command without the required argument:** + + ```bash + bin/console send-email + ``` + + **Output:** + ``` + Error: The recipient email is required. + ``` + +--- + +### Explanation of the Features Used + +1. **Required Arguments**: + - The `recipient` argument is mandatory. If missing, the command displays an error. + +2. **Optional Options**: + - The `--subject` option allows defining the subject of the email. If not specified, a default value ("No subject") is used. + +3. **Simple Validation**: + - The email address is validated using `filter_var` to ensure it has a valid format. + +4. **User Feedback**: + - Clear and simple messages are displayed to guide the user during the command execution. + +## Defining Arguments and Options + +Arguments and options allow developers to customize the behavior of a command based on the parameters passed to it. These concepts are managed by the `CommandArgument` and `CommandOption` classes, respectively. + +--- + +### 1 Arguments + +An **argument** is a positional parameter passed to a command. For example, in the following command: + +```bash +bin/console send-email recipient@example.com +``` + +`recipient@example.com` is an argument. Arguments are defined using the `CommandArgument` class. Here are the main properties of an argument: + +- **Name (`name`)**: The unique name of the argument. +- **Required (`isRequired`)**: Indicates whether the argument is mandatory. +- **Default Value (`defaultValue`)**: The value used if no argument is provided. +- **Description (`description`)**: A brief description of the argument, useful for help messages. + +##### Example of defining an argument: + +```php +use PhpDevCommunity\Console\Argument\CommandArgument; + +new CommandArgument( + 'recipient', // The name of the argument + true, // The argument is required + null, // No default value + 'The email address of the recipient' // Description +); +``` + +If a required argument is not provided, an exception is thrown. + +--- + +### 2 Options + +An **option** is a named parameter, often prefixed with a double dash (`--`) or a shortcut (`-`). For example, in the following command: + +```bash +bin/console send-email recipient@example.com --subject "Meeting Reminder" +``` + +`--subject` is an option. Options are defined using the `CommandOption` class. Here are the main properties of an option: + +- **Name (`name`)**: The full name of the option, used with `--`. +- **Shortcut (`shortcut`)**: A short alias, used with a single dash (`-`). +- **Description (`description`)**: A brief description of the option, useful for help messages. +- **Flag (`isFlag`)**: Indicates whether the option is a simple flag (present or absent) or if it accepts a value. + +##### Example of defining an option: + +```php +use PhpDevCommunity\Console\Option\CommandOption; + +new CommandOption( + 'subject', // The name of the option + 's', // Shortcut + 'The subject of the email', // Description + false // Not a flag, expects a value +); + +new CommandOption( + 'verbose', // The name of the option + 'v', // Shortcut + 'Enable verbose output', // Description + true // This is a flag +); +``` + +An option with a flag does not accept a value; its mere presence indicates that it is enabled. + +--- + +### 3 Usage in a Command + +In a command, arguments and options are defined by overriding the `getArguments()` and `getOptions()` methods from the `CommandInterface`. + +##### Example: + +```php +use PhpDevCommunity\Console\Argument\CommandArgument; +use PhpDevCommunity\Console\Option\CommandOption; + +public function getArguments(): array +{ + return [ + new CommandArgument('recipient', true, null, 'The email address of the recipient'), + ]; +} + +public function getOptions(): array +{ + return [ + new CommandOption('subject', 's', 'The subject of the email', false), + new CommandOption('verbose', 'v', 'Enable verbose output', true), + ]; +} +``` + +In this example: +- `recipient` is a required argument. +- `--subject` (or `-s`) is an option that expects a value. +- `--verbose` (or `-v`) is a flag option. + +--- + +### 4 Validation and Management + +Arguments and options are automatically validated when the command is executed. For instance, if a required argument is missing or an attempt is made to access an undefined option, an exception will be thrown. + +The `InputInterface` allows you to retrieve these parameters in the `execute` method: +- Arguments: `$input->getArgumentValue('recipient')` +- Options: `$input->getOptionValue('subject')` or `$input->hasOption('verbose')` + +This ensures clear and consistent management of the parameters passed within your PHP project. + +## Handling Different Output Types + +Output management provides clear and useful information during command execution. Below is a practical example demonstrating output functionalities. + +### Example: Command `UserReportCommand` + +This command generates a report for a specific user and uses various output features. + +```php +use PhpDevCommunity\Console\Argument\CommandArgument; +use PhpDevCommunity\Console\Command\CommandInterface; +use PhpDevCommunity\Console\InputInterface; +use PhpDevCommunity\Console\Option\CommandOption; +use PhpDevCommunity\Console\Output\ConsoleOutput; +use PhpDevCommunity\Console\OutputInterface; + +class UserReportCommand implements CommandInterface +{ + public function getName(): string + { + return 'user:report'; + } + + public function getDescription(): string + { + return 'Generates a detailed report for a specific user.'; + } + + public function getArguments(): array + { + return [ + new CommandArgument('user_id', true, null, 'The ID of the user to generate the report for'), + ]; + } + + public function getOptions(): array + { + return [ + new CommandOption('verbose', 'v', 'Enable verbose output', true), + new CommandOption('export', 'e', 'Export the report to a file', false), + ]; + } + + public function execute(InputInterface $input, OutputInterface $output): void + { + $console = new ConsoleOutput($output); + + // Main title + $console->title('User Report Generation'); + + // Arguments and options + $userId = $input->getArgumentValue('user_id'); + $verbose = $input->hasOption('verbose'); + $export = $input->hasOption('export') ? $input->getOptionValue('export') : null; + + $console->info("Generating report for User ID: $userId"); + + // Simulating user data retrieval + $console->spinner(); + $userData = [ + 'name' => 'John Doe', + 'email' => 'john.doe@example.com', + 'active' => true, + 'roles' => ['admin', 'user'] + ]; + + if ($verbose) { + $console->success('User data retrieved successfully.'); + } + + // Displaying user data + $console->json($userData); + + // Displaying user roles as a list + $console->listKeyValues([ + 'Name' => $userData['name'], + 'Email' => $userData['email'], + 'Active' => $userData['active'] ? 'Yes' : 'No', + ]); + $console->list($userData['roles']); + + // Table of recent activities + $headers = ['ID', 'Activity', 'Timestamp']; + $rows = [ + ['1', 'Login', '2024-12-22 12:00:00'], + ['2', 'Update Profile', '2024-12-22 12:30:00'], + ]; + $console->table($headers, $rows); + + // Progress bar + for ($i = 0; $i <= 100; $i += 20) { + $console->progressBar(100, $i); + usleep(500000); + } + + // Final result + if ($export) { + $console->success("Report exported to: $export"); + } else { + $console->success('Report generated successfully!'); + } + + // Confirmation for deletion + if ($console->confirm('Do you want to delete this user?')) { + $console->success('User deleted successfully.'); + } else { + $console->warning('User deletion canceled.'); + } + } +} +``` + +#### Features Used + +1. **Rich Messages**: + - `success($message)`: Displays a success message. + - `warning($message)`: Displays a warning message. + - `error($message)`: Displays a critical error message. + - `info($message)`: Displays an informational message. + - `title($title)`: Displays a main title. + +2. **Structured Output**: + - `json($data)`: Displays data in JSON format. + - `list($items)`: Displays a simple list. + - `listKeyValues($data)`: Displays key-value pairs. + - `table($headers, $rows)`: Displays tabular data. + +3. **Progression and Interactivity**: + - `progressBar($total, $current)`: Displays a progress bar. + - `spinner()`: Displays a loading spinner. + - `confirm($question)`: Prompts for user confirmation. + +--- +## License + +This library is open-source software licensed under the [MIT license](LICENSE). \ No newline at end of file diff --git a/bin/console b/bin/console new file mode 100644 index 0000000..dc02cf5 --- /dev/null +++ b/bin/console @@ -0,0 +1,25 @@ +#!/usr/bin/php +run(new CommandParser(), new Output()); +exit($exitCode); \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..37c423d --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "phpdevcommunity/php-console", + "description": "A lightweight PHP library designed to simplify command handling in console applications.", + "version": "1.0.0", + "type": "package", + "autoload": { + "psr-4": { + "PhpDevCommunity\\Console\\": "src", + "Test\\PhpDevCommunity\\Console\\": "tests" + } + }, + "require": { + "php": ">=7.4", + "ext-mbstring": "*", + "ext-json": "*" + }, + "require-dev": { + "phpdevcommunity/unitester": "^0.1.0@alpha" + }, + "license": "MIT", + "authors": [ + { + "name": "F. Michel", + "homepage": "https://www.phpdevcommunity.com" + } + ] +} diff --git a/examples/commands.php b/examples/commands.php new file mode 100755 index 0000000..79b6e33 --- /dev/null +++ b/examples/commands.php @@ -0,0 +1,38 @@ +boot(); +// $container = $kernel->getContainer(); +// $app = $container->get(CommandRunner::class); + +$app = new CommandRunner([ + new FooCommand(), +]); +$exitCode = $app->run(new CommandParser(), new Output()); +exit($exitCode); + + + diff --git a/examples/output.php b/examples/output.php new file mode 100644 index 0000000..6e09a5a --- /dev/null +++ b/examples/output.php @@ -0,0 +1,85 @@ + 'John Doe', + 'email' => 'john.doe@example.com', + 'active' => true, + 'roles' => ['admin', 'user'] +]; + +$console->json($data); +// +$console->spinner(); + +$message = "This is a long message that needs to be automatically wrapped within the box, so it fits neatly without manual line breaks."; +$console->boxed($message); +$console->boxed($message, '.', 2); + +$console->success('The deployment was successful! All application components have been updated and the server is running the latest version. You can access the application and check if all services are functioning correctly.'); +$console->success('The deployment was successful! All application components have been updated and the server is running the latest version.'); + +$console->warning('Warning: The connection to the remote server is unstable! Several attempts to establish a stable connection have failed, and data integrity cannot be guaranteed. Please check the network connection.'); +$console->warning('Warning: You are attempting to run a script with elevated privileges!'); +// + +$console->info('Info: Data export was completed successfully! All requested records have been exported to the specified format and location. You can now download the file or access it from the application dashboard.'); +$console->info('Info: Data export was completed successfully!'); + +$console->error('Critical error encountered! The server encountered an unexpected condition that prevented it from fulfilling the request.'); + +$console->title('My Application Title'); + +$items = ['First item', 'Second item', 'Third item']; +$console->list($items); +$console->numberedList($items); + + +$items = [ + 'Main Item 1', + ['Sub-item 1.1', 'Sub-item 1.2'], + 'Main Item 2', + ['Sub-item 2.1', ['Sub-sub-item 2.1.1']] +]; +$console->indentedList($items); +$console->writeln(''); +$options = [ + 'Username' => 'admin', + 'Password' => '******', + 'Server' => 'localhost', + 'Port' => '3306' +]; +$console->listKeyValues($options); + +$headers = ['ID', 'Name', 'Status']; +$rows = [ + ['1', 'John Doe', 'Active'], + ['2', 'Jane Smith', 'Inactive'], + ['3', 'Emily Johnson', 'Active'] +]; +$console->table($headers, $rows); + +for ($i = 0; $i <= 100; $i++) { + $console->progressBar(100, $i); + usleep(8000); // Simulate some work being done +} + +if ($console->confirm('Are you sure you want to proceed ?')) { + $console->success('Action confirmed.'); +} else { + $console->error('Action canceled.'); +} + +$name = $console->ask('What is your name ?'); +$console->success("Hello, $name!"); + +$password = $console->ask('Enter your demo password', true, true); +$console->success("Your password is: $password"); + +$console->success('Done!'); diff --git a/src/Argument/CommandArgument.php b/src/Argument/CommandArgument.php new file mode 100755 index 0000000..35dc6e8 --- /dev/null +++ b/src/Argument/CommandArgument.php @@ -0,0 +1,50 @@ +name = $name; + $this->isRequired = $isRequired; + $this->defaultValue = $defaultValue; + $this->description = $description; + } + + public function validate($value): void + { + if ($this->isRequired && empty($value)) { + throw new \InvalidArgumentException(sprintf('The required argument "%s" was not provided.', $this->name)); + } + } + + public function getName(): string + { + return $this->name; + } + + public function isRequired(): bool + { + return $this->isRequired; + } + + /** + * @return mixed|null + */ + public function getDefaultValue() + { + return $this->defaultValue; + } + + public function getDescription(): ?string + { + return $this->description; + } + +} \ No newline at end of file diff --git a/src/Command/CommandInterface.php b/src/Command/CommandInterface.php new file mode 100644 index 0000000..fad220b --- /dev/null +++ b/src/Command/CommandInterface.php @@ -0,0 +1,50 @@ + An array of CommandOption. + */ + public function getOptions(): array; + + /** + * Returns the list of required arguments for the command. + * + * @return array An array of CommandArgument. + */ + public function getArguments(): array; + + /** + * Executes the command with the provided inputs. + * + * @param InputInterface $input The inputs for the command. + * @param OutputInterface $output + * @return void + * @throws \InvalidArgumentException If arguments or options are invalid. + * @throws \RuntimeException If an error occurs during execution. + */ + public function execute(InputInterface $input, OutputInterface $output): void; +} diff --git a/src/Command/HelpCommand.php b/src/Command/HelpCommand.php new file mode 100644 index 0000000..52696d4 --- /dev/null +++ b/src/Command/HelpCommand.php @@ -0,0 +1,66 @@ +title('PhpDevCommunity Console - A PHP Console Application'); + + $io->writeColor('Usage:', 'yellow'); + $io->write(\PHP_EOL); + $io->write(' command [options] [arguments]'); + $io->write(\PHP_EOL); + $io->write(\PHP_EOL); + + + $io->writeColor('List of Available Commands:', 'yellow'); + $io->write(\PHP_EOL); + $commands = []; + foreach ($this->commands as $command) { + $commands[$command->getName()] = $command->getDescription(); + } + $io->listKeyValues($commands, true); + } + + /** + * @param CommandInterface[] $commands + */ + public function setCommands(array $commands) + { + $this->commands = $commands; + } + + public function getOptions(): array + { + return []; + } + + public function getArguments(): array + { + return []; + } +} diff --git a/src/CommandParser.php b/src/CommandParser.php new file mode 100755 index 0000000..b433230 --- /dev/null +++ b/src/CommandParser.php @@ -0,0 +1,100 @@ +cmdName = $argv[0] ?? null; + + $ignoreKeys = [0]; + foreach ($argv as $key => $value) { + if (in_array($key, $ignoreKeys, true)) { + continue; + } + + if (self::startsWith($value, '--')) { + $it = explode("=", ltrim($value, '-'), 2); + $optionName = $it[0]; + $optionValue = $it[1] ?? true; + $this->options[$optionName] = $optionValue; + } elseif (self::startsWith($value, '-')) { + $optionName = ltrim($value, '-'); + if (strlen($optionName) > 1) { + $options = str_split($optionName); + foreach ($options as $option) { + $this->options[$option] = true; + } + } else { + $this->options[$optionName] = true; + if (isset($argv[$key + 1]) && !self::startsWith($argv[$key + 1], '-')) { + $ignoreKeys[] = $key + 1; + $this->options[$optionName] = $argv[$key + 1]; + } + } + } else { + $this->arguments[] = $value; + } + } + } + + public function getCommandName(): ?string + { + return $this->cmdName; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function hasOption(string $name): bool + { + return array_key_exists($name, $this->options); + } + + public function getOptionValue(string $name) + { + if (!$this->hasOption($name)) { + throw new InvalidArgumentException(sprintf('Option "%s" is not defined.', $name)); + } + return $this->options[$name]; + } + + public function getArgumentValue(string $name) + { + if (!$this->hasArgument($name)) { + throw new InvalidArgumentException(sprintf('Argument "%s" is not defined.', $name)); + } + return $this->arguments[$name]; + } + + public function hasArgument(string $name): bool + { + return array_key_exists($name, $this->arguments); + } + + private static function startsWith(string $haystack, string $needle): bool + { + return strncmp($haystack, $needle, strlen($needle)) === 0; + } +} diff --git a/src/CommandRunner.php b/src/CommandRunner.php new file mode 100644 index 0000000..a58c6d1 --- /dev/null +++ b/src/CommandRunner.php @@ -0,0 +1,155 @@ +defaultCommand = new HelpCommand(); + foreach ($commands as $command) { + if (!is_subclass_of($command, CommandInterface::class)) { + $commandName = is_object($command) ? get_class($command) : $command; + throw new InvalidArgumentException(sprintf('Command "%s" must implement "%s".', $commandName, CommandInterface::class)); + } + } + $this->commands = array_merge($commands, [$this->defaultCommand]); + $this->defaultCommand->setCommands($this->commands); + } + + public function run(CommandParser $commandParser, OutputInterface $output): int + { + try { + + if ($commandParser->getCommandName() === null || $commandParser->getCommandName() === '--help') { + $this->defaultCommand->execute(new Input($this->defaultCommand->getName(), [], []), $output); + return self::CLI_SUCCESS; + } + + $command = null; + foreach ($this->commands as $currentCommand) { + if ($currentCommand->getName() === $commandParser->getCommandName()) { + $command = $currentCommand; + break; + } + } + + if ($command === null) { + throw new InvalidArgumentException(sprintf('Command "%s" is not defined.', $commandParser->getCommandName())); + } + + if ($commandParser->hasOption('help')) { + $this->showCommandHelp($command, $output); + return self::CLI_SUCCESS; + } + + $this->execute($command, $commandParser, $output); + + return self::CLI_SUCCESS; + + } catch (Throwable $e) { + (new ConsoleOutput($output))->error($e->getMessage()); + return self::CLI_ERROR; + } + + } + + private function execute(CommandInterface $command, CommandParser $commandParser, OutputInterface $output) + { + $argvOptions = []; + + $options = $command->getOptions(); + foreach ($commandParser->getOptions() as $name => $value) { + $hasOption = false; + foreach ($options as $option) { + if ($option->getName() === $name || $option->getShortcut() === $name) { + $hasOption = true; + if (!$option->isFlag() && ($value === true || empty($value))) { + throw new InvalidArgumentException(sprintf('Option "%s" requires a value for command "%s".', $name, $command->getName())); + } + $argvOptions["--{$option->getName()}"] = $value; + break; + } + } + if (!$hasOption) { + throw new InvalidArgumentException(sprintf('Option "%s" is not defined for command "%s".', $name, $command->getName())); + } + } + + $argv = []; + + $arguments = $command->getArguments(); + foreach ($arguments as $key => $argument) { + $key = strval($key); + if ($argument->isRequired() && empty($commandParser->getArgumentValue($key))) { + throw new InvalidArgumentException(sprintf('Argument "%s" is required for command "%s".', $argument->getName(), $command->getName())); + } + if ($commandParser->hasArgument($key)) { + $argv["--{$argument->getName()}"] = $commandParser->getArgumentValue($key); + } + } + + if (count($commandParser->getArguments()) > count($arguments)) { + throw new InvalidArgumentException(sprintf('Too many arguments for command "%s". Expected %d, got %d.', $command->getName(), count($arguments), count($commandParser->getArguments()))); + } + + $command->execute(new Input($commandParser->getCommandName(), $argvOptions, $argv), $output); + } + + private function showCommandHelp(CommandInterface $selectedCommand, OutputInterface $output): void + { + $consoleOutput = new ConsoleOutput($output); + $consoleOutput->writeColor('Description:', 'yellow'); + $consoleOutput->write(PHP_EOL); + $consoleOutput->writeln($selectedCommand->getDescription()); + $consoleOutput->write(PHP_EOL); + + $consoleOutput->writeColor('Arguments:', 'yellow'); + $consoleOutput->write(PHP_EOL); + $arguments = []; + foreach ($selectedCommand->getArguments() as $argument) { + $arguments[$argument->getName()] = $argument->getDescription(); + } + $consoleOutput->listKeyValues($arguments, true); + + $consoleOutput->writeColor('Options:', 'yellow'); + $consoleOutput->write(PHP_EOL); + $options = []; + foreach ($selectedCommand->getOptions() as $option) { + $name = sprintf('--%s', $option->getName()); + if ($option->getShortcut() !== null) { + $name = sprintf('-%s, --%s', $option->getShortcut(), $option->getName()); + } + + if (!$option->isFlag()) { + $name = sprintf('%s=VALUE', $name); + } + $options[$name] = $option->getDescription(); + } + $consoleOutput->listKeyValues($options, true); + } +} diff --git a/src/Input.php b/src/Input.php new file mode 100644 index 0000000..fdc00b5 --- /dev/null +++ b/src/Input.php @@ -0,0 +1,75 @@ +cmdName = $cmdName; + $this->options = $options; + $this->arguments = $arguments; + } + + public function getCommandName(): ?string + { + return $this->cmdName; + } + + public function getOptions(): array + { + return $this->options; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function hasOption(string $name): bool + { + if (!self::startsWith($name, '--')) { + $name = "--$name"; + } + + return array_key_exists($name, $this->options); + } + + public function getOptionValue(string $name) + { + if (!self::startsWith($name, '--')) { + $name = "--$name"; + } + + if (!$this->hasOption($name)) { + throw new \InvalidArgumentException(sprintf('Option "%s" is not defined.', $name)); + } + return $this->options[$name]; + } + + public function getArgumentValue(string $name) + { + if (!$this->hasArgument($name)) { + throw new \InvalidArgumentException(sprintf('Argument "%s" is not defined.', $name)); + } + return $this->arguments[$name]; + } + + public function hasArgument(string $name): bool + { + return array_key_exists($name, $this->arguments); + } + + private static function startsWith(string $haystack, string $needle): bool + { + return strncmp($haystack, $needle, strlen($needle)) === 0; + } +} diff --git a/src/InputInterface.php b/src/InputInterface.php new file mode 100644 index 0000000..c9b3ab9 --- /dev/null +++ b/src/InputInterface.php @@ -0,0 +1,14 @@ +name = $name; + $this->shortcut = $shortcut; + $this->description = $description; + $this->isFlag = $isFlag; + } + + public function getName(): string + { + return $this->name; + } + + public function getShortcut(): ?string + { + return $this->shortcut; + } + + public function getDescription(): ?string + { + return $this->description; + } + + public function isFlag(): bool + { + return $this->isFlag; + } +} \ No newline at end of file diff --git a/src/Output.php b/src/Output.php new file mode 100644 index 0000000..2498124 --- /dev/null +++ b/src/Output.php @@ -0,0 +1,36 @@ +output = $output; + } + + public function write(string $message): void + { + $output = $this->output; + $output($message); + } + + public function writeln(string $message): void + { + $this->write($message); + $this->write(PHP_EOL); + } +} diff --git a/src/Output/ConsoleOutput.php b/src/Output/ConsoleOutput.php new file mode 100755 index 0000000..4470d15 --- /dev/null +++ b/src/Output/ConsoleOutput.php @@ -0,0 +1,342 @@ + '0;30', + 'dark_gray' => '1;30', + 'green' => '0;32', + 'light_green' => '1;32', + 'red' => '0;31', + 'light_red' => '1;31', + 'yellow' => '0;33', + 'light_yellow' => '1;33', + 'blue' => '0;34', + 'dark_blue' => '0;34', + 'light_blue' => '1;34', + 'purple' => '0;35', + 'light_purple' => '1;35', + 'cyan' => '0;36', + 'light_cyan' => '1;36', + 'light_gray' => '0;37', + 'white' => '1;37', + ]; + + const BG_COLORS = [ + 'black' => '40', + 'red' => '41', + 'green' => '42', + 'yellow' => '43', + 'blue' => '44', + 'magenta' => '45', + 'cyan' => '46', + 'light_gray' => '47', + ]; + + private OutputInterface $output; + + public static function create(OutputInterface $output): ConsoleOutput + { + return new self($output); + } + + public function __construct(OutputInterface $output) + { + $this->output = $output; + } + + public function success(string $message): void + { + [$formattedMessage, $lineLength, $color] = $this->formatMessage('OK', $message, 'green'); + $this->outputMessage($formattedMessage, $lineLength, $color); + } + + public function error(string $message): void + { + [$formattedMessage, $lineLength, $color] = $this->formatMessage('ERROR', $message, 'red'); + $this->outputMessage($formattedMessage, $lineLength, $color); + } + + public function warning(string $message): void + { + [$formattedMessage, $lineLength, $color] = $this->formatMessage('WARNING', $message, 'yellow'); + $this->outputMessage($formattedMessage, $lineLength, $color); + } + + public function info(string $message): void + { + [$formattedMessage, $lineLength, $color] = $this->formatMessage('INFO', $message, 'blue'); + $this->outputMessage($formattedMessage, $lineLength, $color); + } + + public function title(string $message): void + { + $consoleWidth = $this->geTerminalWidth(); + $titleLength = mb_strlen($message); + $underline = str_repeat('=', min($consoleWidth, $titleLength)); + + $this->writeColor(PHP_EOL); + $this->writeColor($message); + $this->writeColor(PHP_EOL); + $this->writeColor($underline); + $this->writeColor(PHP_EOL); + } + + public function list(array $items): void + { + foreach ($items as $item) { + $this->writeColor('- ' . $item); + $this->writeColor(PHP_EOL); + } + $this->writeColor(PHP_EOL); + } + + public function listKeyValues(array $items, bool $inlined = false): void + { + $maxKeyLength = 0; + if ($inlined) { + foreach ($items as $key => $value) { + $keyLength = mb_strlen($key); + if ($keyLength > $maxKeyLength) { + $maxKeyLength = $keyLength; + } + } + } + + foreach ($items as $key => $value) { + $key = str_pad($key, $maxKeyLength, ' ', STR_PAD_RIGHT); + $this->writeColor($key, 'green'); + $this->writeColor(' : '); + $this->writeColor($value, 'white'); + $this->writeColor(PHP_EOL); + } + $this->writeColor(PHP_EOL); + } + + public function indentedList(array $items, int $indentLevel = 1): void + { + if ($indentLevel == 1) { + $this->writeColor(PHP_EOL); + } + + foreach ($items as $item) { + if (is_array($item)) { + $this->indentedList($item, $indentLevel + 1); + } else { + $indentation = ''; + if ($indentLevel > 1) { + $indentation = str_repeat(' ', $indentLevel); // Indent with spaces + } + $this->writeColor($indentation . '- ', 'red'); + $this->writeColor($item, 'white'); + $this->writeColor(PHP_EOL); + } + } + + if ($indentLevel == 1) { + $this->writeColor(PHP_EOL); + } + } + + public function numberedList(array $items) + { + foreach ($items as $index => $item) { + $this->writeColor(($index + 1) . '. ', 'white'); + $this->writeColor($item, 'green'); + $this->writeColor(PHP_EOL); + } + $this->writeColor(PHP_EOL); + } + + public function table(array $headers, array $rows): void + { + $columnWidths = array_map(function ($header) { + return mb_strlen($header); + }, $headers); + + foreach ($rows as $row) { + foreach ($row as $index => $column) { + $columnWidths[$index] = max($columnWidths[$index], mb_strlen($column)); + } + } + + foreach ($headers as $index => $header) { + $this->writeColor(str_pad($header, $columnWidths[$index] + 2)); + } + $this->writeColor(PHP_EOL); + + $this->writeColor(str_repeat('-', array_sum($columnWidths) + count($columnWidths) * 2)); + $this->writeColor(PHP_EOL); + + foreach ($rows as $row) { + foreach ($row as $index => $column) { + $this->writeColor(str_pad($column, $columnWidths[$index] + 2)); + } + $this->writeColor(PHP_EOL); + } + } + + public function progressBar(int $total, int $current): void + { + $barWidth = 50; + $progress = ($current / $total) * $barWidth; + $bar = str_repeat('#', (int)$progress) . str_repeat(' ', $barWidth - (int)$progress); + $this->writeColor(sprintf("\r[%s] %d%%", $bar, ($current / $total) * 100)); + if ($current === $total) { + $this->writeColor(PHP_EOL); + } + } + + public function confirm(string $message): bool + { + $this->writeColor($message . ' [y/n]: ', 'yellow'); + + $handle = fopen('php://stdin', 'r'); + $input = trim(fgets($handle)); + fclose($handle); + + return in_array($input, ['y', 'yes', 'Y', 'YES'], true); + } + + public function ask(string $question, bool $hidden = false, bool $required = false): string + { + $this->writeColor($question . ': ', 'cyan'); + + if ($hidden) { + if (strncasecmp(PHP_OS, 'WIN', 3) == 0) { + throw new \RuntimeException('Windows platform is not supported for hidden input'); + } else { + system('stty -echo'); + $input = trim(fgets(STDIN)); + system('stty echo'); + $this->writeColor(PHP_EOL); + } + } else { + $handle = fopen('php://stdin', 'r'); + $input = trim(fgets($handle)); + fclose($handle); + } + + if ($required && empty($input)) { + throw new \InvalidArgumentException('Response cannot be empty'); + } + + return $input; + } + + public function spinner(int $duration = 3): void + { + $spinnerChars = ['|', '/', '-', '\\']; + $time = microtime(true); + while ((microtime(true) - $time) < $duration) { + foreach ($spinnerChars as $char) { + $this->writeColor("\r$char"); + usleep(100000); + } + } + $this->writeColor("\r"); + } + + public function json(array $data): void + { + $jsonOutput = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + + if (json_last_error() !== JSON_ERROR_NONE) { + $this->writeColor("Error encoding JSON: " . json_last_error_msg() . PHP_EOL, 'red'); + return; + } + $this->writeColor($jsonOutput . PHP_EOL); + } + + public function boxed(string $message, string $borderChar = '*', int $padding = 1): void + { + $this->writeColor(PHP_EOL); + $lineLength = mb_strlen($message); + $boxWidth = $this->geTerminalWidth(); + if ($lineLength > $boxWidth) { + $lineLength = $boxWidth - ($padding * 2) - 2; + } + $lines = explode('|', wordwrap($message, $lineLength, '|', true)); + $border = str_repeat($borderChar, $lineLength + ($padding * 2) + 2); + + $this->writeColor($border . PHP_EOL); + foreach ($lines as $line) { + $strPad = str_repeat(' ', $padding); + $this->writeColor($borderChar . $strPad . str_pad($line, $lineLength) . $strPad . $borderChar . PHP_EOL); + } + $this->writeColor($border . PHP_EOL); + $this->writeColor(PHP_EOL); + } + + public function writeColor(string $message, ?string $color = null, ?string $background = null): void + { + + $formattedMessage = ''; + + if ($color) { + $formattedMessage .= "\033[" . self::FOREGROUND_COLORS[$color] . 'm'; + } + if ($background) { + $formattedMessage .= "\033[" . self::BG_COLORS[$background] . 'm'; + } + + $formattedMessage .= $message . "\033[0m"; + + $this->write($formattedMessage); + } + + public function write(string $message): void + { + $this->output->write($message); + } + + public function writeln(string $message): void + { + $this->output->writeln($message); + } + + private function outputMessage($formattedMessage, int $lineLength, string $color): void + { + $this->writeColor(PHP_EOL); + $this->writeColor(str_repeat(' ', $lineLength), 'white', $color); + $this->writeColor(PHP_EOL); + + if (is_string($formattedMessage)) { + $formattedMessage = [$formattedMessage]; + } + + foreach ($formattedMessage as $line) { + $this->writeColor($line, 'white', $color); + } + + $this->writeColor(PHP_EOL); + $this->writeColor(str_repeat(' ', $lineLength), 'white', $color); + $this->writeColor(PHP_EOL); + $this->writeColor(PHP_EOL); + } + + private function formatMessage(string $prefix, string $message, string $color): array + { + $formattedMessage = sprintf('[%s] %s', $prefix, trim($message)); + $lineLength = mb_strlen($formattedMessage); + $consoleWidth = $this->geTerminalWidth(); + + if ($lineLength > $consoleWidth) { + $lineLength = $consoleWidth; + $lines = explode('|', wordwrap($formattedMessage, $lineLength, '|', true)); + $formattedMessage = array_map(function ($line) use ($lineLength) { + return str_pad($line, $lineLength); + }, $lines); + } + return [$formattedMessage, $lineLength, $color]; + } + private function geTerminalWidth(): int + { + return ((int)exec('tput cols') ?? 85 - 5); + } +} diff --git a/src/OutputInterface.php b/src/OutputInterface.php new file mode 100644 index 0000000..64407b0 --- /dev/null +++ b/src/OutputInterface.php @@ -0,0 +1,9 @@ +writeln('Test OK'); + $output->writeln('ARGUMENTS: ' . json_encode($input->getArguments())); + $output->writeln('OPTIONS: ' . json_encode($input->getOptions())); + } +} diff --git a/tests/CommandArgumentTest.php b/tests/CommandArgumentTest.php new file mode 100755 index 0000000..89f16b8 --- /dev/null +++ b/tests/CommandArgumentTest.php @@ -0,0 +1,93 @@ +testValidateThrowsExceptionIfRequiredAndValueIsEmpty(); + $this->testValidateDoesNotThrowExceptionIfNotRequiredAndValueIsEmpty(); + $this->testValidateDoesNotThrowExceptionIfRequiredAndValueIsNotEmpty(); + $this->testGetNameReturnsCorrectValue(); + $this->testIsRequiredReturnsCorrectValue(); + $this->testGetDefaultValueReturnsCorrectValue(); + $this->testGetDescriptionReturnsCorrectValue(); + } + + public function testValidateThrowsExceptionIfRequiredAndValueIsEmpty() + { + $arg = new CommandArgument('test', true); + + $this->expectException(\InvalidArgumentException::class, function () use ($arg) { + $arg->validate(''); + }, 'The required argument "test" was not provided.'); + + } + + public function testValidateDoesNotThrowExceptionIfNotRequiredAndValueIsEmpty() + { + $arg = new CommandArgument('test'); + + $arg->validate(''); + + $this->assertTrue(true); + } + + public function testValidateDoesNotThrowExceptionIfRequiredAndValueIsNotEmpty() + { + $arg = new CommandArgument('test', true); + + $arg->validate('value'); + + $this->assertTrue(true); + } + + public function testGetNameReturnsCorrectValue() + { + $arg = new CommandArgument('test'); + + $this->assertEquals('test', $arg->getName()); + } + + public function testIsRequiredReturnsCorrectValue() + { + $arg = new CommandArgument('test', true); + + $this->assertTrue($arg->isRequired()); + + $arg = new CommandArgument('test'); + + $this->assertFalse($arg->isRequired()); + } + + public function testGetDefaultValueReturnsCorrectValue() + { + $arg = new CommandArgument('test', false, 'default'); + + $this->assertEquals('default', $arg->getDefaultValue()); + } + + public function testGetDescriptionReturnsCorrectValue() + { + $arg = new CommandArgument('test', false, null, 'description'); + + $this->assertEquals('description', $arg->getDescription()); + } + +} \ No newline at end of file diff --git a/tests/CommandOptionTest.php b/tests/CommandOptionTest.php new file mode 100755 index 0000000..fa8249a --- /dev/null +++ b/tests/CommandOptionTest.php @@ -0,0 +1,65 @@ +testConstructor(); + $this->testGetName(); + $this->testGetShortcut(); + $this->testGetDescription(); + $this->testIsFlag(); + } + + public function testConstructor(): void + { + $option = new CommandOption('foo', 'f', 'description', true); + + $this->assertEquals('foo', $option->getName()); + $this->assertEquals('f', $option->getShortcut()); + $this->assertEquals('description', $option->getDescription()); + $this->assertTrue($option->isFlag()); + } + + public function testGetName(): void + { + $option = new CommandOption('foo'); + $this->assertEquals('foo', $option->getName()); + } + + public function testGetShortcut(): void + { + $option = new CommandOption('foo', 'f'); + $this->assertEquals('f', $option->getShortcut()); + } + + public function testGetDescription(): void + { + $option = new CommandOption('foo', null, 'description'); + $this->assertEquals('description', $option->getDescription()); + } + + public function testIsFlag(): void + { + $option = new CommandOption('foo', null, null, true); + $this->assertTrue($option->isFlag()); + } + +} \ No newline at end of file diff --git a/tests/CommandParserTest.php b/tests/CommandParserTest.php new file mode 100755 index 0000000..4c54bcd --- /dev/null +++ b/tests/CommandParserTest.php @@ -0,0 +1,81 @@ +testCommandName(); + $this->testOptions(); + $this->testArguments(); + $this->testHasOption(); + $this->testGetOptionValue(); + $this->testGetArgumentValue(); + } + + public function testCommandName() + { + $parser = new CommandParser(self::createArgv(['foo', '--bar=baz'])); + $this->assertEquals('foo', $parser->getCommandName()); + } + + public function testOptions() + { + $parser = new CommandParser(self::createArgv(['foo', '--bar=baz', '--qux'])); + $this->assertEquals(['bar' => 'baz', 'qux' => true], $parser->getOptions()); + } + + public function testArguments() + { + $parser = new CommandParser(self::createArgv(['foo', 'bar', 'baz'])); + $this->assertEquals(['bar', 'baz'], $parser->getArguments()); + } + + public function testHasOption() + { + $parser = new CommandParser(self::createArgv(['foo', '--bar=baz'])); + $this->assertTrue($parser->hasOption('bar')); + $this->assertFalse($parser->hasOption('qux')); + } + + public function testGetOptionValue() + { + $parser = new CommandParser(self::createArgv(['foo', '--bar=baz'])); + $this->assertEquals('baz', $parser->getOptionValue('bar')); + } + + public function testGetArgumentValue() + { + $parser = new CommandParser(self::createArgv(['foo', 'bar', 'baz'])); + $this->assertEquals('bar', $parser->getArgumentValue(0)); + $this->assertEquals('baz', $parser->getArgumentValue(1)); + } + + public function testHasArgument() + { + $parser = new CommandParser(self::createArgv(['foo', 'bar', 'baz'])); + $this->assertTrue($parser->hasArgument(0)); + $this->assertTrue($parser->hasArgument(1)); + $this->assertFalse($parser->hasArgument(2)); + } + + private static function createArgv(array $argv): array + { + return array_merge(['bin/console'], $argv); + } +} \ No newline at end of file diff --git a/tests/CommandTest.php b/tests/CommandTest.php new file mode 100755 index 0000000..f6291fa --- /dev/null +++ b/tests/CommandTest.php @@ -0,0 +1,96 @@ +testGetName(); + $this->testGetDescription(); + $this->testGetOptions(); + $this->testGetArguments(); + $this->testExecute(); + } + + public function testGetName(): void + { + $command = new FooCommand(); + $this->assertEquals('foo', $command->getName()); + } + + public function testGetDescription(): void + { + $command = new FooCommand(); + $this->assertEquals('Performs the foo operation with optional parameters.', $command->getDescription()); + } + + public function testGetOptions(): void + { + $command = new FooCommand(); + $options = $command->getOptions(); + $this->assertEquals(2, count($options)); + + $this->assertInstanceOf(CommandOption::class, $options[0]); + $this->assertEquals('verbose', $options[0]->getName()); + $this->assertEquals('v', $options[0]->getShortcut()); + $this->assertEquals('Enable verbose output', $options[0]->getDescription()); + $this->assertTrue($options[0]->isFlag()); + + $this->assertInstanceOf(CommandOption::class, $options[1]); + $this->assertEquals('output', $options[1]->getName()); + $this->assertEquals('o', $options[1]->getShortcut()); + $this->assertEquals('Specify output file', $options[1]->getDescription()); + $this->assertFalse($options[1]->isFlag()); + } + + public function testGetArguments(): void + { + $command = new FooCommand(); + $arguments = $command->getArguments(); + $this->assertEquals(1, count($arguments)); + $this->assertInstanceOf(CommandArgument::class, $arguments[0]); + $this->assertEquals('input', $arguments[0]->getName()); + $this->assertFalse($arguments[0]->isRequired()); + } + + public function testExecute(): void + { + $input = new Input('foo', ['verbose' => true, 'output' => 'output.txt'], ['input' => 'foo']); + $lines = 0; + $output = new Output(function (string $message) use (&$lines) { + if ($lines === 0) { + $this->assertEquals('Test OK', $message); + } + if ($lines === 2) { + $this->assertEquals('ARGUMENTS: {"input":"foo"}', $message); + } + if ($lines === 4) { + $this->assertEquals('OPTIONS: {"verbose":true,"output":"output.txt"}', $message); + } + $lines++; + }); + $command = new FooCommand(); + $command->execute($input, $output); + + $this->assertEquals(6, $lines); + } +} \ No newline at end of file diff --git a/tests/InputTest.php b/tests/InputTest.php new file mode 100755 index 0000000..58bcfff --- /dev/null +++ b/tests/InputTest.php @@ -0,0 +1,87 @@ +testGetCommandName(); + $this->testGetOptions(); + $this->testGetArguments(); + $this->testHasOption(); + $this->testHasArgument(); + $this->testGetOptionValue(); + $this->testGetArgumentValue(); + + } + + public function testGetCommandName() + { + $input = new Input('test', [], []); + $this->assertEquals('test', $input->getCommandName()); + } + + public function testGetOptions() + { + $options = ['--option1' => 'value1', '--option2' => 'value2']; + $input = new Input('test', $options, []); + $this->assertEquals($options, $input->getOptions()); + } + + public function testGetArguments() + { + $arguments = ['argument1' => 'value1', 'argument2' => 'value2']; + $input = new Input('test', [], $arguments); + $this->assertEquals($arguments, $input->getArguments()); + } + + public function testHasOption() + { + $input = new Input('test', ['--option' => 'value'], []); + $this->assertTrue($input->hasOption('option')); + $this->assertTrue($input->hasOption('--option')); + $this->assertFalse($input->hasOption('invalid')); + } + + public function testGetOptionValue() + { + $input = new Input('test', ['--option' => 'value'], []); + $this->assertEquals('value', $input->getOptionValue('option')); + $this->assertEquals('value', $input->getOptionValue('--option')); + $this->expectException(\InvalidArgumentException::class, function () use ($input) { + $input->getOptionValue('invalid'); + }); + } + + public function testGetArgumentValue() + { + $input = new Input('test', [], ['argument' => 'value']); + $this->assertEquals('value', $input->getArgumentValue('argument')); + $this->expectException(\InvalidArgumentException::class, function () use ($input) { + $input->getArgumentValue('invalid'); + }); + } + + public function testHasArgument() + { + $input = new Input('test', [], ['argument' => 'value']); + $this->assertTrue($input->hasArgument('argument')); + $this->assertFalse($input->hasArgument('invalid')); + } +} \ No newline at end of file diff --git a/tests/OutputTest.php b/tests/OutputTest.php new file mode 100755 index 0000000..11d74c5 --- /dev/null +++ b/tests/OutputTest.php @@ -0,0 +1,51 @@ +testWrite(); + $this->testWriteln(); + } + + public function testWrite() + { + $output = new Output(function ($message) { + $this->assertEquals('Hello, world!', $message); + }); + + $output->write('Hello, world!'); + } + + public function testWriteln() + { + $lines = 0; + $output = new Output(function ($message) use(&$lines) { + if ($lines === 0) { + $this->assertEquals('Hello, world!', $message); + } + if ($lines === 1) { + $this->assertEquals(PHP_EOL, $message); + } + $lines++; + }); + + $output->writeln('Hello, world!'); + $this->assertEquals(2, $lines); + } +} \ No newline at end of file