├── src ├── Command │ ├── InvalidCommandExpression.php │ ├── Command.php │ └── ExpressionParser.php ├── Input │ ├── InputOption.php │ └── InputArgument.php ├── HyphenatedInputResolver.php └── Application.php ├── SECURITY.md ├── .github ├── FUNDING.yml └── workflows │ └── ci.yml ├── composer.json ├── CONTRIBUTING.md ├── LICENSE └── README.md /src/Command/InvalidCommandExpression.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InvalidCommandExpression extends \InvalidArgumentException 11 | { 12 | } 13 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Only the latest stable release is supported. 6 | 7 | ## Reporting a Vulnerability 8 | 9 | To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). 10 | 11 | Tidelift will coordinate the fix and disclosure. 12 | -------------------------------------------------------------------------------- /src/Input/InputOption.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InputOption extends \Symfony\Component\Console\Input\InputOption 11 | { 12 | private $description; 13 | 14 | public function setDescription($description) 15 | { 16 | $this->description = $description; 17 | } 18 | 19 | public function getDescription(): string 20 | { 21 | return $this->description ?: parent::getDescription(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Input/InputArgument.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | class InputArgument extends \Symfony\Component\Console\Input\InputArgument 11 | { 12 | private $description; 13 | 14 | public function setDescription($description) 15 | { 16 | $this->description = $description; 17 | } 18 | 19 | public function getDescription(): string 20 | { 21 | return $this->description ?: parent::getDescription(); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mnapoli # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: "packagist/mnapoli/silly" 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 13 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mnapoli/silly", 3 | "description": "Silly CLI micro-framework based on Symfony Console", 4 | "keywords": ["cli", "console", "framework", "micro-framework", "silly", "psr-11"], 5 | "license": "MIT", 6 | "type": "library", 7 | "autoload": { 8 | "psr-4": { 9 | "Silly\\": "src/" 10 | } 11 | }, 12 | "autoload-dev": { 13 | "psr-4": { 14 | "Silly\\Test\\": "tests/" 15 | } 16 | }, 17 | "require": { 18 | "php": ">=7.4", 19 | "symfony/console": "~3.0|~4.0|~5.0|~6.0|~7.0", 20 | "php-di/invoker": "~2.0", 21 | "psr/container": "^1.0|^2.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^6.4|^7|^8|^9|^10", 25 | "mnapoli/phpunit-easymock": "~1.0", 26 | "friendsofphp/php-cs-fixer": "^2.12" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: contributing 3 | --- 4 | # Contributing 5 | 6 | First of all, **thank you** for contributing! 7 | 8 | Silly is developed on GitHub: [`github.com/mnapoli/silly`](https://github.com/mnapoli/silly), you are welcome to open issues and pull requests. If you want to get involved more seriously in the project, I would happily discuss with you about your ideas and give you commit access. 9 | 10 | Here are a few rules to follow in order to ease code reviews and merging: 11 | 12 | - follow [PSR-1](http://www.php-fig.org/psr/1/) and [PSR-2](http://www.php-fig.org/psr/2/) 13 | - run the test suite 14 | - write (or update) tests when applicable 15 | - write documentation for new features 16 | - use [commit messages that make sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) 17 | 18 | When creating your pull request on GitHub, please write a description which gives the context and/or explains why you are creating it. 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Matthieu Napoli 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 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: PHPUnit 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | tests: 9 | name: Tests 10 | strategy: 11 | matrix: 12 | include: 13 | - php: '7.4' 14 | - php: '8.0' 15 | - php: '8.1' 16 | - php: '8.2' 17 | - php: '8.3' 18 | fail-fast: false 19 | 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | - name: Setup PHP 26 | uses: shivammathur/setup-php@v2 27 | with: 28 | coverage: xdebug 29 | php-version: "${{ matrix.php }}" 30 | - name: Get composer cache directory 31 | id: composer-cache 32 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 33 | - name: Cache composer dependencies 34 | uses: actions/cache@v1 35 | with: 36 | path: ${{ steps.composer-cache.outputs.dir }} 37 | key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} 38 | restore-keys: ${{ runner.os }}-composer- 39 | - name: Install 40 | run: | 41 | composer install -n 42 | if [ "${{ matrix.mode }}" = "low-deps" ]; then composer update --prefer-lowest --prefer-stable -n; fi; 43 | - name: Tests 44 | run: vendor/bin/phpunit 45 | -------------------------------------------------------------------------------- /src/HyphenatedInputResolver.php: -------------------------------------------------------------------------------- 1 | call($callable, ['dry-run' => true])` will inject the boolean `true` 13 | * for a parameter named either `$dryrun` or `$dryRun`. 14 | */ 15 | class HyphenatedInputResolver implements ParameterResolver 16 | { 17 | public function getParameters( 18 | ReflectionFunctionAbstract $reflection, 19 | array $providedParameters, 20 | array $resolvedParameters 21 | ): array { 22 | $parameters = []; 23 | 24 | foreach ($reflection->getParameters() as $index => $parameter) { 25 | $parameters[strtolower($parameter->name)] = $index; 26 | } 27 | 28 | foreach ($providedParameters as $name => $value) { 29 | $normalizedName = strtolower(str_replace('-', '', $name)); 30 | 31 | // Skip parameters that do not exist with the normalized name 32 | if (! array_key_exists($normalizedName, $parameters)) { 33 | continue; 34 | } 35 | 36 | $normalizedParameterIndex = $parameters[$normalizedName]; 37 | 38 | // Skip parameters already resolved 39 | if (array_key_exists($normalizedParameterIndex, $resolvedParameters)) { 40 | continue; 41 | } 42 | 43 | $resolvedParameters[$normalizedParameterIndex] = $value; 44 | } 45 | 46 | return $resolvedParameters; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Command/Command.php: -------------------------------------------------------------------------------- 1 | getDefinition(); 24 | 25 | $this->setDescription($description); 26 | 27 | foreach ($argumentAndOptionDescriptions as $name => $value) { 28 | if (strpos($name, '--') === 0) { 29 | $name = substr($name, 2); 30 | $this->setOptionDescription($definition, $name, $value); 31 | } else { 32 | $this->setArgumentDescription($definition, $name, $value); 33 | } 34 | } 35 | 36 | return $this; 37 | } 38 | 39 | /** 40 | * Define default values for the arguments of the command. 41 | * 42 | * @param array $defaults Default argument values. 43 | * 44 | * @return $this 45 | * 46 | * @api 47 | */ 48 | public function defaults(array $defaults = []) 49 | { 50 | $definition = $this->getDefinition(); 51 | 52 | foreach ($defaults as $name => $default) { 53 | if ($definition->hasArgument($name)) { 54 | $input = $definition->getArgument($name); 55 | } elseif ($definition->hasOption($name)) { 56 | $input = $definition->getOption($name); 57 | } else { 58 | throw new \InvalidArgumentException("Unable to set default for [{$name}]. It does not exist as an argument or option."); 59 | } 60 | 61 | $input->setDefault($default); 62 | } 63 | 64 | return $this; 65 | } 66 | 67 | private function setArgumentDescription(InputDefinition $definition, $name, $description) 68 | { 69 | $argument = $definition->getArgument($name); 70 | if ($argument instanceof InputArgument) { 71 | $argument->setDescription($description); 72 | } 73 | } 74 | 75 | private function setOptionDescription(InputDefinition $definition, $name, $description) 76 | { 77 | $argument = $definition->getOption($name); 78 | if ($argument instanceof InputOption) { 79 | $argument->setDescription($description); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | currentMenu: home 3 | --- 4 | Silly CLI micro-framework based on Symfony Console. 5 | 6 | [![Build Status](https://img.shields.io/travis/mnapoli/silly/master.svg?style=flat-square)](https://travis-ci.org/mnapoli/silly) 7 | [![Coverage Status](https://img.shields.io/coveralls/mnapoli/silly/master.svg?style=flat-square)](https://coveralls.io/r/mnapoli/silly?branch=master) 8 | [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/mnapoli/silly.svg?style=flat-square)](https://scrutinizer-ci.com/g/mnapoli/silly/?branch=master) 9 | [![Packagist](https://img.shields.io/packagist/dt/mnapoli/silly.svg?maxAge=2592000)](https://packagist.org/packages/mnapoli/silly) 10 | 11 | Professional support for Silly [is available via Tidelift](https://tidelift.com/subscription/pkg/packagist-mnapoli-silly?utm_source=packagist-mnapoli-silly&utm_medium=referral&utm_campaign=readme) 12 | 13 | - [Video introduction in french](https://www.youtube.com/watch?v=aoE1FDN5_8s) 14 | 15 | ## Installation 16 | 17 | ```bash 18 | $ composer require mnapoli/silly 19 | ``` 20 | 21 | ## Usage 22 | 23 | Example of a Silly application: 24 | 25 | ```php 26 | use Symfony\Component\Console\Output\OutputInterface; 27 | 28 | $app = new Silly\Application(); 29 | 30 | $app->command('greet [name] [--yell]', function ($name, $yell, OutputInterface $output) { 31 | $text = $name ? "Hello, $name" : "Hello"; 32 | 33 | if ($yell) { 34 | $text = strtoupper($text); 35 | } 36 | 37 | $output->writeln($text); 38 | }); 39 | 40 | $app->run(); 41 | ``` 42 | 43 | Running the application is the same as running any other Symfony Console application: 44 | 45 | ```bash 46 | $ php application.php greet 47 | Hello 48 | $ php application.php greet john --yell 49 | HELLO JOHN 50 | $ php application.php greet --yell john 51 | HELLO JOHN 52 | ``` 53 | 54 | `Silly\Application` extends `Symfony\Console\Application` and can be used wherever Symfony's Application can. 55 | 56 | ## Documentation 57 | 58 | - [Command definition](docs/command-definition.md) 59 | - [Command callables](docs/command-callables.md) 60 | - [Console helpers](docs/helpers.md) 61 | - [Dependency injection](docs/dependency-injection.md) 62 | - [The PHP-DI edition](docs/php-di.md) 63 | - [The Pimple edition](docs/pimple.md) 64 | 65 | ## Do more 66 | 67 | Silly is just an implementation over the Symfony Console. Read [the Symfony documentation](http://symfony.com/doc/current/components/console/introduction.html) to learn everything you can do with it. 68 | 69 | ## Example applications 70 | 71 | Interested in seeing examples of Silly applications? Have a look at this short selection: 72 | 73 | - [Bref](https://github.com/mnapoli/bref/blob/c11662125d3d6cf3f96ee82c9e6fc60d9bcbbfdd/bref) 74 | - [Laravel Valet](https://github.com/laravel/valet/blob/7ed0280374340b30f1e2698fe85d7db543570f57/cli/valet.php) 75 | - [Blacksmith](https://github.com/mpociot/blacksmith/blob/320e97b9677f9e885d1f478593143f329afb9510/blacksmith) 76 | - [Documentarian](https://github.com/mpociot/documentarian/blob/34189ff3357aa3b013930b471410f135f09792de/documentarian) 77 | 78 | ## Contributing 79 | 80 | See the [CONTRIBUTING](CONTRIBUTING.md) file. 81 | -------------------------------------------------------------------------------- /src/Command/ExpressionParser.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class ExpressionParser 14 | { 15 | public function parse($expression) 16 | { 17 | $tokens = explode(' ', $expression); 18 | $tokens = array_map('trim', $tokens); 19 | $tokens = array_values(array_filter($tokens)); 20 | 21 | if (count($tokens) === 0) { 22 | throw new InvalidCommandExpression('The expression was empty'); 23 | } 24 | 25 | $name = array_shift($tokens); 26 | 27 | $arguments = []; 28 | $options = []; 29 | 30 | foreach ($tokens as $token) { 31 | if ($this->startsWith($token, '--')) { 32 | throw new InvalidCommandExpression('An option must be enclosed by brackets: [--option]'); 33 | } 34 | 35 | if ($this->isOption($token)) { 36 | $options[] = $this->parseOption($token); 37 | } else { 38 | $arguments[] = $this->parseArgument($token); 39 | } 40 | } 41 | 42 | return [ 43 | 'name' => $name, 44 | 'arguments' => $arguments, 45 | 'options' => $options, 46 | ]; 47 | } 48 | 49 | private function isOption($token) 50 | { 51 | return $this->startsWith($token, '[-'); 52 | } 53 | 54 | private function parseArgument($token) 55 | { 56 | if ($this->endsWith($token, ']*')) { 57 | $mode = InputArgument::IS_ARRAY; 58 | $name = trim($token, '[]*'); 59 | } elseif ($this->endsWith($token, '*')) { 60 | $mode = InputArgument::IS_ARRAY | InputArgument::REQUIRED; 61 | $name = trim($token, '*'); 62 | } elseif ($this->startsWith($token, '[')) { 63 | $mode = InputArgument::OPTIONAL; 64 | $name = trim($token, '[]'); 65 | } else { 66 | $mode = InputArgument::REQUIRED; 67 | $name = $token; 68 | } 69 | 70 | return new InputArgument($name, $mode); 71 | } 72 | 73 | private function parseOption($token) 74 | { 75 | $token = trim($token, '[]'); 76 | 77 | // Shortcut [-y|--yell] 78 | if (strpos($token, '|') !== false) { 79 | list($shortcut, $token) = explode('|', $token, 2); 80 | $shortcut = ltrim($shortcut, '-'); 81 | } else { 82 | $shortcut = null; 83 | } 84 | 85 | $name = ltrim($token, '-'); 86 | 87 | if ($this->endsWith($token, '=]*')) { 88 | $mode = InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY; 89 | $name = substr($name, 0, -3); 90 | } elseif ($this->endsWith($token, '=')) { 91 | $mode = InputOption::VALUE_REQUIRED; 92 | $name = rtrim($name, '='); 93 | } else { 94 | $mode = InputOption::VALUE_NONE; 95 | } 96 | 97 | return new InputOption($name, $shortcut, $mode); 98 | } 99 | 100 | private function startsWith($haystack, $needle) 101 | { 102 | return substr($haystack, 0, strlen($needle)) === $needle; 103 | } 104 | 105 | private function endsWith($haystack, $needle) 106 | { 107 | return substr($haystack, -strlen($needle)) === $needle; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Application.php: -------------------------------------------------------------------------------- 1 | 30 | */ 31 | class Application extends SymfonyApplication 32 | { 33 | /** 34 | * @var ExpressionParser 35 | */ 36 | private $expressionParser; 37 | 38 | /** 39 | * @var InvokerInterface 40 | */ 41 | private $invoker; 42 | 43 | /** 44 | * @var ContainerInterface|null 45 | */ 46 | private $container; 47 | 48 | public function __construct($name = 'UNKNOWN', $version = 'UNKNOWN') 49 | { 50 | $this->expressionParser = new ExpressionParser(); 51 | $this->invoker = new Invoker($this->createParameterResolver()); 52 | 53 | parent::__construct($name, $version); 54 | } 55 | 56 | /** 57 | * Define a CLI command using a string expression and a callable. 58 | * 59 | * @param string $expression Defines the arguments and options of the command. 60 | * @param callable|string|array $callable Called when the command is called. 61 | * When using a container, this can be a "pseudo-callable" 62 | * i.e. the name of the container entry to invoke. 63 | * 64 | * @param array $aliases An array of aliases for the command. 65 | * 66 | * @return Command 67 | */ 68 | public function command($expression, $callable, array $aliases = []) 69 | { 70 | $this->assertCallableIsValid($callable); 71 | 72 | $commandFunction = function (InputInterface $input, OutputInterface $output) use ($callable) { 73 | $parameters = array_merge( 74 | [ 75 | // Injection by parameter name 76 | 'input' => $input, 77 | 'output' => $output, 78 | // Injections by type-hint 79 | InputInterface::class => $input, 80 | OutputInterface::class => $output, 81 | Input::class => $input, 82 | Output::class => $output, 83 | SymfonyStyle::class => new SymfonyStyle($input, $output), 84 | ], 85 | // Arguments and options are injected by parameter names 86 | $input->getArguments(), 87 | $input->getOptions() 88 | ); 89 | 90 | if ($callable instanceof \Closure) { 91 | $callable = $callable->bindTo($this, $this); 92 | } 93 | 94 | try { 95 | return $this->invoker->call($callable, $parameters); 96 | } catch (InvocationException $e) { 97 | throw new \RuntimeException(sprintf("Impossible to call the '%s' command: %s", $input->getFirstArgument(), $e->getMessage()), 0, $e); 98 | } 99 | }; 100 | 101 | $command = $this->createCommand($expression, $commandFunction); 102 | $command->setAliases($aliases); 103 | 104 | $command->defaults($this->defaultsViaReflection($command, $callable)); 105 | 106 | $this->add($command); 107 | 108 | return $command; 109 | } 110 | 111 | /** 112 | * Set up the application to use a container to resolve callables. 113 | * 114 | * Only commands that are *not* PHP callables will be fetched from the container. 115 | * Commands that are PHP callables are not affected (which is what we want). 116 | * 117 | * *Optionally*, you can also enable dependency injection in the callable parameters: 118 | * 119 | * $application->command('greet', function (Psr\Log\LoggerInterface $logger) { 120 | * $logger->info('I am greeting'); 121 | * }); 122 | * 123 | * Set `$injectByTypeHint` to `true` to make Silly fetch container entries by their 124 | * type-hint, i.e. call `$container->get('Psr\Log\LoggerInterface')`. 125 | * 126 | * Set `$injectByParameterName` to `true` to make Silly fetch container entries by 127 | * the parameter name, i.e. call `$container->get('logger')`. 128 | * 129 | * If you set both to `true`, it will first look using the type-hint, then using 130 | * the parameter name. 131 | * 132 | * In case of conflict with a command parameters, the command parameter is injected 133 | * in priority over dependency injection. 134 | * 135 | * @param ContainerInterface $container Container implementing PSR-11 136 | * @param bool $injectByTypeHint 137 | * @param bool $injectByParameterName 138 | */ 139 | public function useContainer( 140 | ContainerInterface $container, 141 | $injectByTypeHint = false, 142 | $injectByParameterName = false 143 | ) { 144 | $this->container = $container; 145 | 146 | $resolver = $this->createParameterResolver(); 147 | if ($injectByTypeHint) { 148 | $resolver->appendResolver(new TypeHintContainerResolver($container)); 149 | } 150 | if ($injectByParameterName) { 151 | $resolver->appendResolver(new ParameterNameContainerResolver($container)); 152 | } 153 | 154 | $this->invoker = new Invoker($resolver, $container); 155 | } 156 | 157 | /** 158 | * Helper to run a sub-command from a command. 159 | * 160 | * @param string $command Command that should be run. 161 | * @param OutputInterface|null $output The output to use. If not provided, the output will be silenced. 162 | * 163 | * @return int 0 if everything went fine, or an error code 164 | */ 165 | public function runCommand($command, ?OutputInterface $output = null) 166 | { 167 | $input = new StringInput($command); 168 | 169 | $command = $this->find($this->getCommandName($input)); 170 | 171 | return $command->run($input, $output ?: new NullOutput()); 172 | } 173 | 174 | /** 175 | * Returns the container that has been configured, or null. 176 | * 177 | * @return ContainerInterface|null 178 | */ 179 | public function getContainer() 180 | { 181 | return $this->container; 182 | } 183 | 184 | /** 185 | * @return InvokerInterface 186 | */ 187 | public function getInvoker() 188 | { 189 | return $this->invoker; 190 | } 191 | 192 | public function setInvoker(InvokerInterface $invoker) 193 | { 194 | $this->invoker = $invoker; 195 | } 196 | 197 | /** 198 | * @param string $expression 199 | * @return Command 200 | */ 201 | private function createCommand($expression, callable $callable) 202 | { 203 | $result = $this->expressionParser->parse($expression); 204 | 205 | $command = new Command($result['name']); 206 | $command->getDefinition()->addArguments($result['arguments']); 207 | $command->getDefinition()->addOptions($result['options']); 208 | 209 | $command->setCode($callable); 210 | 211 | return $command; 212 | } 213 | 214 | private function assertCallableIsValid($callable) 215 | { 216 | if ($this->container) { 217 | return; 218 | } 219 | 220 | if ($this->isStaticCallToNonStaticMethod($callable)) { 221 | list($class, $method) = $callable; 222 | 223 | $message = "['{$class}', '{$method}'] is not a callable because '{$method}' is a static method."; 224 | $message .= " Either use [new {$class}(), '{$method}'] or configure a dependency injection container that supports autowiring like PHP-DI."; 225 | 226 | throw new \InvalidArgumentException($message); 227 | } 228 | } 229 | 230 | /** 231 | * @param Command $command 232 | * @param callable $callable 233 | * @return array 234 | */ 235 | private function defaultsViaReflection($command, $callable) 236 | { 237 | if (! is_callable($callable)) { 238 | return []; 239 | } 240 | 241 | $function = CallableReflection::create($callable); 242 | 243 | $definition = $command->getDefinition(); 244 | 245 | $defaults = []; 246 | 247 | foreach ($function->getParameters() as $parameter) { 248 | if (! $parameter->isDefaultValueAvailable()) { 249 | continue; 250 | } 251 | 252 | $parameterName = $parameter->name; 253 | $hyphenatedCaseName = $this->fromCamelCase($parameterName); 254 | 255 | if ($definition->hasArgument($hyphenatedCaseName) || $definition->hasOption($hyphenatedCaseName)) { 256 | $parameterName = $hyphenatedCaseName; 257 | } 258 | 259 | if (! $definition->hasArgument($parameterName) && ! $definition->hasOption($parameterName)) { 260 | continue; 261 | } 262 | 263 | $defaults[$parameterName] = $parameter->getDefaultValue(); 264 | } 265 | 266 | return $defaults; 267 | } 268 | 269 | /** 270 | * Convert from camel case to hyphenated case. 271 | * 272 | * @see http://stackoverflow.com/questions/1993721/how-to-convert-camelcase-to-camel-case 273 | * @param string $input 274 | * @return string 275 | */ 276 | private function fromCamelCase($input) 277 | { 278 | preg_match_all('!([A-Z][A-Z0-9]*(?=$|[A-Z][a-z0-9])|[A-Za-z][a-z0-9]+)!', $input, $matches); 279 | $ret = $matches[0]; 280 | 281 | foreach ($ret as &$match) { 282 | $match = $match == strtoupper($match) ? strtolower($match) : lcfirst($match); 283 | } 284 | 285 | return implode('-', $ret); 286 | } 287 | 288 | /** 289 | * @return ResolverChain 290 | */ 291 | private function createParameterResolver() 292 | { 293 | return new ResolverChain([ 294 | new AssociativeArrayResolver, 295 | new HyphenatedInputResolver, 296 | new TypeHintResolver, 297 | ]); 298 | } 299 | 300 | /** 301 | * Check if the callable represents a static call to a non-static method. 302 | * 303 | * @param mixed $callable 304 | * @return bool 305 | */ 306 | private function isStaticCallToNonStaticMethod($callable) 307 | { 308 | if (is_array($callable) && is_string($callable[0])) { 309 | list($class, $method) = $callable; 310 | $reflection = new \ReflectionMethod($class, $method); 311 | 312 | return ! $reflection->isStatic(); 313 | } 314 | 315 | return false; 316 | } 317 | } 318 | --------------------------------------------------------------------------------