* Jordi Boggiano * * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace Composer\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use JsonSchema\Validator; use Composer\Config; use Composer\Factory; use Composer\Json\JsonFile; use Composer\Json\JsonValidationException; class ConfigCommand extends Command { /** * @var Composer\Json\JsonFile */ protected $configFile; /** * {@inheritDoc} */ protected function configure() { $this ->setName('config') ->setDescription('Set config options') ->setDefinition(array( new InputOption('global', 'g', InputOption::VALUE_NONE, 'Set this as a global config settings.'), new InputOption('editor', 'e', InputOption::VALUE_NONE, 'Open editor'), new InputOption('list', 'l', InputOption::VALUE_NONE, 'List configuration settings'), new InputOption('file', 'f', InputOption::VALUE_REQUIRED, 'If you want to choose a different composer.json or config.json'), new InputArgument('setting-key', null, 'Setting key'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), )) // @todo Document ->setHelp(<<getOption('global') && $input->getOption('file')) { throw new \RuntimeException('Cannot use both, you must pick to edit either the global config or path to composer.json'); } // Get the local composer.json, global config.json, or if the user // passed in a file to use $this->configFile = $input->getOption('global') ? (Factory::createConfig()->get('home') . '/config.json') : (null !== $input->getOption('file') ? $input->getOption('file') : 'composer.json'); $this->configFile = new JsonFile($this->configFile); if (!$this->configFile->exists()) { touch($this->configFile->getPath()); // If you read an empty file, Composer throws an error // Toss some of the defaults in there $defaults = Config::$defaultConfig; $defaults['repositories'] = Config::$defaultRepositories; $this->configFile->write($defaults); } } /** * {@inheritDoc} */ protected function execute(InputInterface $input, OutputInterface $output) { // Open file in editor if ($input->getOption('editor')) { // @todo Find a way to use another editor $editor = system("bash -cl 'echo \$EDITOR'"); system($editor . ' ' . $this->configFile->getPath() . ' > `tty`'); return 0; } // List the configuration of the file settings if ($input->getOption('list')) { $this->displayFileContents($this->configFile->read(), $output); return 0; } // If the user enters in a config variable, parse it and save to file if ($input->getArgument('setting-key')) { if (null === $input->getArgument('setting-value')) { throw new \RuntimeException('You must include a setting value.'); } /** * The user needs the ability to add a repository with one command. * For example "config -g repository.foo 'vcs http://example.com' */ $configSettings = $this->configFile->read(); // what is current in the config $settings = array(); // This will what will be merged into the above $values = $input->getArgument('setting-value'); // what the user is trying to add/change // Checking for each known config value is going to make this method very large // what is a better way to do this? // repositories.foo if (preg_match('/^repositories\.(.+)/', $input->getArgument('setting-key'), $matches)) { if (2 !== count($values)) { throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com'); } $setting = $this->parseSetting($input->getArgument('setting-key'), array( 'type' => $values[0], 'url' => $values[1], )); // Could there be a better way to do this? $settings = array_merge_recursive($configSettings, $setting); $this->validateSchema($settings); } // process-timeout elseif (preg_match('/^process-timeout/', $input->getArgument('setting-key'))) { if (1 !== count($values)) { throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300'); } if (!is_numeric($values[0])) { throw new \RuntimeException(sprintf('"%s" is not a number.', $values[0])); } $setting = $this->parseSetting('config.'.$input->getArgument('setting-key'), (integer) $values[0]); $settings = array_merge($configSettings, $setting); $this->validateSchema($settings); } // Make sure we have something to write to disk if (!count($settings)) { $output->writeln('Trying to update a setting that is supported with this command.'); return 0; } // Make confirmation if ($input->isInteractive()) { $dialog = $this->getHelperSet()->get('dialog'); $output->writeln(JsonFile::encode($settings)); if (!$dialog->askConfirmation($output, $dialog->getQuestion('Do you confirm?', 'yes', '?'), true)) { $output->writeln('Command Aborted by User'); return 1; } } $this->configFile->write($settings); } } /** * {@inheritDoc} */ protected function interact(InputInterface $input, OutputInterface $output) { } /** * Display the contents of the file in a pretty formatted way * * @param array $contents * @param OutputInterface $output * @param integer $depth * @param string|null $k */ protected function displayFileContents(array $contents, OutputInterface $output, &$depth = 0, $k = null) { // @todo Look into a way to refactor this code, as it is right now, I // don't like it, also the name of the function could be better foreach ($contents as $key => $value) { if (is_array($value)) { $depth++; $k .= $key . '.'; $this->displayFileContents($value, $output, $depth, $k); if (substr_count($k,'.') > 1) { $k = str_split($k,strrpos($k,'.',-2)); $k = $k[0] . '.'; } else { $k = null; } $depth--; continue; } $output->writeln('[' . $k . $key . '] ' . $value . ''); } } /** * This function will take a setting key (a.b.c) and return an * array that matches this * * @param string $key * @param string $value * @return array */ protected function parseSetting($key, $value) { $parts = array_reverse(explode('.', $key)); $tmp = array(); for($i=0;$iproperties->name, $schemaData->properties->description ); $validator = new Validator(); $validator->check(json_decode(json_encode($data)), $schemaData); if (!$validator->isValid()) { $errors = array(); foreach ((array) $validator->getErrors() as $error) { $errors[] = ($error['property'] ? $error['property'].' : ' : '').$error['message']; } throw new JsonValidationException('"'.$this->configFile->getPath().'" does not match the expected JSON schema'."\n". implode("\n",$errors)); } return true; } }