Add support for editing top level properties and extra values, replaces #2415, fixes #1411, fixes #2384
parent
3186b5eeca
commit
135783299a
|
@ -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
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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}
|
||||
*/
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -2050,7 +2050,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
|
|||
$manipulator = new JsonManipulator('{
|
||||
"foo": "bar"
|
||||
}');
|
||||
|
||||
|
||||
$this->assertTrue($manipulator->addMainKey('bar', '$1baz'));
|
||||
$this->assertEquals('{
|
||||
"foo": "bar",
|
||||
|
@ -2069,7 +2069,7 @@ class JsonManipulatorTest extends \PHPUnit_Framework_TestCase
|
|||
}
|
||||
', $manipulator->getContents());
|
||||
}
|
||||
|
||||
|
||||
public function testUpdateMainKey()
|
||||
{
|
||||
$manipulator = new JsonManipulator('{
|
||||
|
@ -2142,7 +2142,68 @@ 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('{
|
||||
|
|
Loading…
Reference in New Issue