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
The `config` command allows you to edit some basic Composer settings in either
the local `composer.json` file or the global `config.json` file.
The `config` command allows you to edit composer config settings and repositories
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
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
`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.
### 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"}'
```
### 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
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\Factory;
use Composer\Json\JsonFile;
use Composer\Semver\VersionParser;
use Composer\Package\BasePackage;
/**
* @author Joshua Estes <Joshua.Estes@iostudio.com>
@ -74,8 +76,10 @@ class ConfigCommand extends BaseCommand
new InputArgument('setting-value', InputArgument::IS_ARRAY, 'Setting value'),
))
->setHelp(<<<EOT
This command allows you to edit some basic composer settings in either the
local composer.json file or the global config.json file.
This command allows you to edit composer config settings and repositories
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:
@ -227,6 +231,8 @@ EOT
// show the value if no value is provided
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();
if (preg_match('/^repos?(?:itories)?(?:\.(.+))?/', $settingKey, $matches)) {
if (empty($matches[1])) {
@ -240,7 +246,11 @@ EOT
}
} elseif (strpos($settingKey, '.')) {
$bits = explode('.', $settingKey);
$data = $data['config'];
if ($bits[0] === 'extra') {
$data = $rawData;
} else {
$data = $data['config'];
}
$match = false;
foreach ($bits as $bit) {
$key = isset($key) ? $key.'.'.$bit : $bit;
@ -259,6 +269,8 @@ EOT
$value = $data;
} elseif (isset($data['config'][$settingKey])) {
$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 {
throw new \RuntimeException($settingKey.' is not defined');
}
@ -387,44 +399,67 @@ EOT
),
);
foreach ($uniqueConfigValues as $name => $callbacks) {
if ($settingKey === $name) {
if ($input->getOption('unset')) {
return $this->configSource->removeConfigSetting($settingKey);
}
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 $this->configSource->addConfigSetting($settingKey, $normalizer($values[0]));
}
if ($input->getOption('unset') && (isset($uniqueConfigValues[$settingKey]) || isset($multiConfigValues[$settingKey]))) {
return $this->configSource->removeConfigSetting($settingKey);
}
if (isset($uniqueConfigValues[$settingKey])) {
return $this->handleSingleValue($settingKey, $uniqueConfigValues[$settingKey], $values, 'addConfigSetting');
}
if (isset($multiConfigValues[$settingKey])) {
return $this->handleMultiValue($settingKey, $multiConfigValues[$settingKey], $values, 'addConfigSetting');
}
foreach ($multiConfigValues as $name => $callbacks) {
if ($settingKey === $name) {
if ($input->getOption('unset')) {
return $this->configSource->removeConfigSetting($settingKey);
}
// handle properties
$uniqueProps = array(
'name' => array('is_string', function ($val) { return $val; }),
'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;
if (true !== $validation = $validator($values)) {
throw new \RuntimeException(sprintf(
'%s is an invalid value'.($validation ? ' ('.$validation.')' : ''),
json_encode($values)
));
}
return true;
},
function ($vals) {
return $vals;
},
),
'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
@ -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');
}
// 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
if (preg_match('/^platform\.(.+)/', $settingKey, $matches)) {
if ($input->getOption('unset')) {
@ -500,6 +544,36 @@ EOT
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
*

View File

@ -50,6 +50,21 @@ interface ConfigSourceInterface
*/
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
*

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}
*/

View File

@ -163,12 +163,30 @@ class JsonManipulator
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)
{
$decoded = JsonFile::parseJson($this->contents);
$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);
}
@ -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) {
if ($subName !== null) {
$curVal = json_decode($matches[2], true);
if (!is_array($curVal)) {
$curVal = array();
}
$curVal[$subName] = $value;
$value = $curVal;
}
@ -275,7 +296,7 @@ class JsonManipulator
}
$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);
}
@ -374,6 +395,34 @@ class JsonManipulator
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)
{
if (is_array($data)) {

View File

@ -2143,6 +2143,67 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
', $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()
{
$manipulator = new JsonManipulator('{