1
0
Fork 0

Add support for editing top level properties and extra values, replaces #2415, fixes #1411, fixes #2384

pull/5267/head
Jordi Boggiano 2016-04-27 10:48:21 +01:00
parent 3186b5eeca
commit 135783299a
6 changed files with 309 additions and 44 deletions

View File

@ -499,8 +499,10 @@ sudo -H composer self-update
## config ## config
The `config` command allows you to edit some basic Composer settings in either The `config` command allows you to edit composer config settings and repositories
the local `composer.json` file or the global `config.json` file. in either the local `composer.json` file or the global `config.json` file.
Additionally it lets you edit most properties in the local `composer.json`.
```sh ```sh
php composer.phar config --list php composer.phar config --list
@ -514,6 +516,11 @@ php composer.phar config --list
configuration value. For settings that can take an array of values (like configuration value. For settings that can take an array of values (like
`github-protocols`), more than one setting-value arguments are allowed. `github-protocols`), more than one setting-value arguments are allowed.
You can also edit the values of the following properties:
`description`, `homepage`, `keywords`, `license`, `minimum-stability`,
`name`, `prefer-stable`, `type` and `version`.
See the [Config](06-config.md) chapter for valid configuration options. See the [Config](06-config.md) chapter for valid configuration options.
### Options ### Options
@ -547,6 +554,18 @@ If your repository requires more configuration options, you can instead pass its
php composer.phar config repositories.foo '{"type": "vcs", "url": "http://svn.example.org/my-project/", "trunk-path": "master"}' php composer.phar config repositories.foo '{"type": "vcs", "url": "http://svn.example.org/my-project/", "trunk-path": "master"}'
``` ```
### Modifying Extra Values
In addition to modifying the config section, the `config` command also supports making
changes to the extra section by using it the following way:
```sh
php composer.phar config extra.foo.bar value
```
The dots indicate array nesting, a max depth of 3 levels is allowed though. The above
would set `"extra": { "foo": { "bar": "value" } }`.
## create-project ## create-project
You can use Composer to create new projects from an existing package. This is You can use Composer to create new projects from an existing package. This is

View File

@ -22,6 +22,8 @@ use Composer\Config;
use Composer\Config\JsonConfigSource; use Composer\Config\JsonConfigSource;
use Composer\Factory; use Composer\Factory;
use Composer\Json\JsonFile; use Composer\Json\JsonFile;
use Composer\Semver\VersionParser;
use Composer\Package\BasePackage;
/** /**
* @author Joshua Estes <Joshua.Estes@iostudio.com> * @author Joshua Estes <Joshua.Estes@iostudio.com>
@ -74,8 +76,10 @@ class ConfigCommand extends BaseCommand
new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'), new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'),
)) ))
->setHelp(<<<EOT ->setHelp(<<<EOT
This command allows you to edit some basic composer settings in either the This command allows you to edit composer config settings and repositories
local composer.json file or the global config.json file. in either the local composer.json file or the global config.json file.
Additionally it lets you edit most properties in the local composer.json.
To set a config setting: To set a config setting:
@ -227,6 +231,8 @@ EOT
// show the value if no value is provided // show the value if no value is provided
if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) { if (array() === $input->getArgument('setting-value') && !$input->getOption('unset')) {
$properties = array('name', 'type', 'description', 'homepage', 'version', 'minimum-stability', 'prefer-stable', 'keywords', 'license', 'extra');
$rawData = $this->configFile->read();
$data = $this->config->all(); $data = $this->config->all();
if (preg_match('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) { if (preg_match('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) {
if (empty($matches[1])) { if (empty($matches[1])) {
@ -240,7 +246,11 @@ EOT
} }
} elseif (strpos($settingKey, '.')) { } elseif (strpos($settingKey, '.')) {
$bits = explode('.', $settingKey); $bits = explode('.', $settingKey);
$data = $data['config']; if ($bits[0] === 'extra') {
$data = $rawData;
} else {
$data = $data['config'];
}
$match = false; $match = false;
foreach ($bits as $bit) { foreach ($bits as $bit) {
$key = isset($key) ? $key.'.'.$bit : $bit; $key = isset($key) ? $key.'.'.$bit : $bit;
@ -259,6 +269,8 @@ EOT
$value = $data; $value = $data;
} elseif (isset($data['config'][$settingKey])) { } elseif (isset($data['config'][$settingKey])) {
$value = $this->config->get($settingKey, $input->getOption('absolute') ? 0 : Config::RELATIVE_PATHS); $value = $this->config->get($settingKey, $input->getOption('absolute') ? 0 : Config::RELATIVE_PATHS);
} elseif (in_array($settingKey, $properties, true) && isset($rawData[$settingKey])) {
$value = $rawData[$settingKey];
} else { } else {
throw new \RuntimeException($settingKey.' is not defined'); throw new \RuntimeException($settingKey.' is not defined');
} }
@ -387,44 +399,67 @@ EOT
), ),
); );
foreach ($uniqueConfigValues as $name => $callbacks) { if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) {
if ($settingKey === $name) { return $this->configSource->removeConfigSetting($settingKey);
if ($input->getOption('unset')) { }
return $this->configSource->removeConfigSetting($settingKey); if (isset($uniqueConfigValues[$settingKey])) {
} return $this->handleSingleValue($settingKey, $uniqueConfigValues[$settingKey], $values, 'addConfigSetting');
}
list($validator, $normalizer) = $callbacks; if (isset($multiConfigValues[$settingKey])) {
if (1 !== count($values)) { return $this->handleMultiValue($settingKey, $multiConfigValues[$settingKey], $values, 'addConfigSetting');
throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300');
}
if (true !== $validation = $validator($values[0])) {
throw new \RuntimeException(sprintf(
'"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''),
$values[0]
));
}
return $this->configSource->addConfigSetting($settingKey, $normalizer($values[0]));
}
} }
foreach ($multiConfigValues as $name => $callbacks) { // handle properties
if ($settingKey === $name) { $uniqueProps = array(
if ($input->getOption('unset')) { 'name' => array('is_string', function ($val) { return $val; }),
return $this->configSource->removeConfigSetting($settingKey); 'type' => array('is_string', function ($val) { return $val; }),
} 'description' => array('is_string', function ($val) { return $val; }),
'homepage' => array('is_string', function ($val) { return $val; }),
'version' => array('is_string', function ($val) { return $val; }),
'minimum-stability' => array(
function ($val) { return isset(BasePackage::$stabilities[VersionParser::normalizeStability($val)]); },
function ($val) { return VersionParser::normalizeStability($val); }
),
'prefer-stable' => array($booleanValidator, $booleanNormalizer),
);
$multiProps = array(
'keywords' => array(
function ($vals) {
if (!is_array($vals)) {
return 'array expected';
}
list($validator, $normalizer) = $callbacks; return true;
if (true !== $validation = $validator($values)) { },
throw new \RuntimeException(sprintf( function ($vals) {
'%s is an invalid value'.($validation ? ' ('.$validation.')' : ''), return $vals;
json_encode($values) },
)); ),
} 'license' => array(
function ($vals) {
if (!is_array($vals)) {
return 'array expected';
}
return $this->configSource->addConfigSetting($settingKey, $normalizer($values)); return true;
} },
function ($vals) {
return $vals;
},
),
);
if ($input->getOption('global') && (isset($uniqueProps[$settingKey]) || isset($multiProps[$settingKey]) || substr($settingKey, 0, 6) === 'extra.')) {
throw new \InvalidArgumentException('The '.$settingKey.' property can not be set in the global config.json file. Use `composer global config` to apply changes to the global composer.json');
}
if ($input->getOption('unset') && (isset($uniqueProps[$settingKey]) || isset($multiProps[$settingKey]))) {
return $this->configSource->removeProperty($settingKey);
}
if (isset($uniqueProps[$settingKey])) {
return $this->handleSingleValue($settingKey, $uniqueProps[$settingKey], $values, 'addProperty');
}
if (isset($multiProps[$settingKey])) {
return $this->handleMultiValue($settingKey, $multiProps[$settingKey], $values, 'addProperty');
} }
// handle repositories // handle repositories
@ -456,6 +491,15 @@ EOT
throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs https://bar.com'); throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs https://bar.com');
} }
// handle extra
if (preg_match('/^extra\.(.+)/', $settingKey, $matches)) {
if ($input->getOption('unset')) {
return $this->configSource->removeProperty($settingKey);
}
return $this->configSource->addProperty($settingKey, $values[0]);
}
// handle platform // handle platform
if (preg_match('/^platform\.(.+)/', $settingKey, $matches)) { if (preg_match('/^platform\.(.+)/', $settingKey, $matches)) {
if ($input->getOption('unset')) { if ($input->getOption('unset')) {
@ -500,6 +544,36 @@ EOT
throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command'); throw new \InvalidArgumentException('Setting '.$settingKey.' does not exist or is not supported by this command');
} }
protected function handleSingleValue($key, array $callbacks, array $values, $method)
{
list($validator, $normalizer) = $callbacks;
if (1 !== count($values)) {
throw new \RuntimeException('You can only pass one value. Example: php composer.phar config process-timeout 300');
}
if (true !== $validation = $validator($values[0])) {
throw new \RuntimeException(sprintf(
'"%s" is an invalid value'.($validation ? ' ('.$validation.')' : ''),
$values[0]
));
}
return call_user_func(array($this->configSource, $method), $key, $normalizer($values[0]));
}
protected function handleMultiValue($key, array $callbacks, array $values, $method)
{
list($validator, $normalizer) = $callbacks;
if (true !== $validation = $validator($values)) {
throw new \RuntimeException(sprintf(
'%s is an invalid value'.($validation ? ' ('.$validation.')' : ''),
json_encode($values)
));
}
return call_user_func(array($this->configSource, $method), $key, $normalizer($values));
}
/** /**
* Display the contents of the file in a pretty formatted way * Display the contents of the file in a pretty formatted way
* *

View File

@ -50,6 +50,21 @@ interface ConfigSourceInterface
*/ */
public function removeConfigSetting($name); public function removeConfigSetting($name);
/**
* Add a property
*
* @param string $name Name
* @param string $value Value
*/
public function addProperty($name, $value);
/**
* Remove a property
*
* @param string $name
*/
public function removeProperty($name);
/** /**
* Add a package link * Add a package link
* *

View File

@ -114,6 +114,53 @@ class JsonConfigSource implements ConfigSourceInterface
}); });
} }
/**
* {@inheritdoc}
*/
public function addProperty($name, $value)
{
$this->manipulateJson('addProperty', $name, $value, function (&$config, $key, $val) {
if (substr($key, 0, 6) === 'extra.') {
$bits = explode('.', $key);
$last = array_pop($bits);
$arr =& $config['extra'];
foreach ($bits as $bit) {
if (!isset($arr[$bit])) {
$arr[$bit] = array();
}
$arr =& $arr[$bit];
}
$arr[$last] = $val;
} else {
$config[$key] = $val;
}
});
}
/**
* {@inheritdoc}
*/
public function removeProperty($name)
{
$authConfig = $this->authConfig;
$this->manipulateJson('removeProperty', $name, function (&$config, $key) {
if (substr($key, 0, 6) === 'extra.') {
$bits = explode('.', $key);
$last = array_pop($bits);
$arr =& $config['extra'];
foreach ($bits as $bit) {
if (!isset($arr[$bit])) {
return;
}
$arr =& $arr[$bit];
}
unset($arr[$last]);
} else {
unset($config[$key]);
}
});
}
/** /**
* {@inheritdoc} * {@inheritdoc}
*/ */

View File

@ -163,12 +163,30 @@ class JsonManipulator
return $this->removeSubNode('config', $name); return $this->removeSubNode('config', $name);
} }
public function addProperty($name, $value)
{
if (substr($name, 0, 6) === 'extra.') {
return $this->addSubNode('extra', substr($name, 6), $value);
}
return $this->addMainKey($name, $value);
}
public function removeProperty($name)
{
if (substr($name, 0, 6) === 'extra.') {
return $this->removeSubNode('extra', substr($name, 6));
}
return $this->removeMainKey($name);
}
public function addSubNode($mainNode, $name, $value) public function addSubNode($mainNode, $name, $value)
{ {
$decoded = JsonFile::parseJson($this->contents); $decoded = JsonFile::parseJson($this->contents);
$subName = null; $subName = null;
if (in_array($mainNode, array('config', 'repositories')) && false !== strpos($name, '.')) { if (in_array($mainNode, array('config', 'repositories', 'extra')) && false !== strpos($name, '.')) {
list($name, $subName) = explode('.', $name, 2); list($name, $subName) = explode('.', $name, 2);
} }
@ -211,6 +229,9 @@ class JsonManipulator
$children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) { $children = preg_replace_callback('{("'.preg_quote($name).'"\s*:\s*)('.self::$JSON_VALUE.')(,?)}', function ($matches) use ($name, $subName, $value, $that) {
if ($subName !== null) { if ($subName !== null) {
$curVal = json_decode($matches[2], true); $curVal = json_decode($matches[2], true);
if (!is_array($curVal)) {
$curVal = array();
}
$curVal[$subName] = $value; $curVal[$subName] = $value;
$value = $curVal; $value = $curVal;
} }
@ -275,7 +296,7 @@ class JsonManipulator
} }
$subName = null; $subName = null;
if (in_array($mainNode, array('config', 'repositories')) && false !== strpos($name, '.')) { if (in_array($mainNode, array('config', 'repositories', 'extra')) && false !== strpos($name, '.')) {
list($name, $subName) = explode('.', $name, 2); list($name, $subName) = explode('.', $name, 2);
} }
@ -374,6 +395,34 @@ class JsonManipulator
return true; return true;
} }
public function removeMainKey($key)
{
$decoded = JsonFile::parseJson($this->contents);
if (!isset($decoded[$key])) {
return true;
}
// key exists already
$regex = '{^(\s*\{\s*(?:'.self::$JSON_STRING.'\s*:\s*'.self::$JSON_VALUE.'\s*,\s*)*?)'.
'('.preg_quote(JsonFile::encode($key)).'\s*:\s*'.self::$JSON_VALUE.')\s*,?\s*(.*)}s';
if ($this->pregMatch($regex, $this->contents, $matches)) {
// invalid match due to un-regexable content, abort
if (!@json_decode('{'.$matches[2].'}')) {
return false;
}
$this->contents = $matches[1] . $matches[3];
if (preg_match('#^\{\s*\}\s*$#', $this->contents)) {
$this->contents = "{\n}";
}
return true;
}
return false;
}
public function format($data, $depth = 0) public function format($data, $depth = 0)
{ {
if (is_array($data)) { if (is_array($data)) {

View File

@ -2050,7 +2050,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
$manipulator = new JsonManipulator('{ $manipulator = new JsonManipulator('{
"foo": "bar" "foo": "bar"
}'); }');
$this->assertTrue($manipulator->addMainKey('bar', '$1baz')); $this->assertTrue($manipulator->addMainKey('bar', '$1baz'));
$this->assertEquals('{ $this->assertEquals('{
"foo": "bar", "foo": "bar",
@ -2069,7 +2069,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
} }
', $manipulator->getContents()); ', $manipulator->getContents());
} }
public function testUpdateMainKey() public function testUpdateMainKey()
{ {
$manipulator = new JsonManipulator('{ $manipulator = new JsonManipulator('{
@ -2142,7 +2142,68 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
} }
', $manipulator->getContents()); ', $manipulator->getContents());
} }
public function testRemoveMainKey()
{
$manipulator = new JsonManipulator('{
"repositories": [
{
"package": {
"require": {
"this/should-not-end-up-in-root-require": "~2.0"
},
"require-dev": {
"this/should-not-end-up-in-root-require-dev": "~2.0"
}
}
}
],
"require": {
"package/a": "*",
"package/b": "*",
"package/c": "*"
},
"foo": "bar",
"require-dev": {
"package/d": "*"
}
}');
$this->assertTrue($manipulator->removeMainKey('repositories'));
$this->assertEquals('{
"require": {
"package/a": "*",
"package/b": "*",
"package/c": "*"
},
"foo": "bar",
"require-dev": {
"package/d": "*"
}
}
', $manipulator->getContents());
$this->assertTrue($manipulator->removeMainKey('foo'));
$this->assertEquals('{
"require": {
"package/a": "*",
"package/b": "*",
"package/c": "*"
},
"require-dev": {
"package/d": "*"
}
}
', $manipulator->getContents());
$this->assertTrue($manipulator->removeMainKey('require'));
$this->assertTrue($manipulator->removeMainKey('require-dev'));
$this->assertEquals('{
}
', $manipulator->getContents());
}
public function testIndentDetection() public function testIndentDetection()
{ {
$manipulator = new JsonManipulator('{ $manipulator = new JsonManipulator('{