Add options to configure repository priorities

main
Jordi Boggiano 4 years ago
parent 59c831c2f8
commit b6bad4eef6
No known key found for this signature in database
GPG Key ID: 7BBD42C429EC80BC

@ -8,6 +8,7 @@
* Added support for parallel downloads of package metadata and zip files, this requires that the curl extension is present
* Added much clearer dependency resolution error reporting for common error cases
* Added support for TTY mode on Linux/OSX/WSL so that script handlers now run in interactive mode
* Added `only`, `exclude` and `canonical` options to all repositories, see [repository priorities](https://getcomposer.org/repoprio) for details
* Added support for lib-zip platform package
* Added `pre-operations-exec` event to be fired before the packages get installed/upgraded/removed
* Added `pre-pool-create` event to be fired before the package pool for the dependency solver is created, which lets you modify the list of packages going in

@ -2,7 +2,7 @@
## For composer CLI users
- If a packages exists in a higher priority repository, it will now be entirely ignored in lower priority repositories.
- If a packages exists in a higher priority repository, it will now be entirely ignored in lower priority repositories. See [repository priorities](https://getcomposer.org/repoprio) for details.
- Invalid PSR-0 / PSR-4 class configurations will not autoload anymore in optimized-autoloader mode, as per the warnings introduced in 1.10
- Package names now must comply to our naming guidelines or Composer will abort, as per the warnings introduced in 1.8.1
- Removed --no-suggest flag as it is not needed anymore

@ -41,7 +41,7 @@ be preferred.
A repository is a package source. It's a list of packages/versions. Composer
will look in all your repositories to find the packages your project requires.
By default only the Packagist repository is registered in Composer. You can
By default only the Packagist.org repository is registered in Composer. You can
add more repositories to your project by declaring them in `composer.json`.
Repositories are only available to the root package and the repositories
@ -49,6 +49,12 @@ defined in your dependencies will not be loaded. Read the
[FAQ entry](faqs/why-can't-composer-load-repositories-recursively.md) if you
want to learn why.
When resolving dependencies, packages are looked up from repositories from
top to bottom, and by default as soon as a package is found in one Composer
stops looking in other repositories. Read the
[repository priorities](articles/repository-priorities.md) article for more
details and to see how to change this behavior.
## Types
### Composer
@ -62,6 +68,17 @@ In the case of packagist, that file is located at `/packages.json`, so the URL o
the repository would be `repo.packagist.org`. For `example.org/packages.json` the
repository URL would be `example.org`.
```json
{
"repositories": [
{
"type": "composer",
"url": "https://example.org"
}
]
}
```
#### packages
The only required field is `packages`. The JSON structure is as follows:

@ -0,0 +1,94 @@
<!--
tagline: Configure which packages are found in which repositories
-->
# Repository priorities
## Canonical repositories
When Composer resolves dependencies it will look up a given package in the
topmost repository. If that repository does not contain the package, it
goes on to the next one, until one repository contains it and the process ends.
Canonical repositories are better for a few reasons:
- Performance wise, it is more efficient to stop looking for a package once it
has been found somewhere. It also avoids loading duplicate packages in case
the same package is present in several of your repositories.
- Security wise, it is safer to treat them canonically as it means that your most
important repositories will return the packages you expect them to always. Let's
say you have a private repository which is not canonical, and you require your
private package `foo/bar ^2.0` for example. Now if someone publishes
`foo/bar 2.999` to packagist.org, suddenly Composer will pick that package as it
has a higher version than your latest release (say 2.4.3), and you end up install
something you may not have meant to. If the private repository is canonical
however, that 2.999 version from packagist.org will not be considered at all.
There are however a few cases where you may want to specifically load some packages
from a given repository, but not all. Or you may want a given repository to not be
canonical, and to be only preferred if it has higher package versions than the
repositories defined below.
## Default behavior
By default in Composer 2.x all repositories are canonical. Composer 1.x treated
all repositories as non-canonical.
Another default is that the packagist.org repository is always added implicitly
as the last repository, unless you [disable it](../05-repositories.md#disabling-packagist-org).
## Making repositories non-canonical
You can add the canonical option to any repository to disable this default behavior
and make sure Composer keeps looking in other repositories, even if that repository
contains a given package.
```json
{
"repositories": [
{
"type": "composer",
"url": "https://example.org",
"canonical": false
}
]
}
```
## Filtering packages
You can also filter packages which a repository will be able to load, either by
selecting which you want, or by excluding those you do not want.
For example here we want to pick only the `foo/bar` and all the packages from
`some-vendor/` from this composer repository.
```json
{
"repositories": [
{
"type": "composer",
"url": "https://example.org",
"only": ["foo/bar", "some-vendor/*"]
}
]
}
```
And in this other example we exclude `toy/package` from a path repository, which
we may not want to load in this project.
```json
{
"repositories": [
{
"type": "composer",
"url": "https://example.org",
"exclude": ["toy/package"]
}
]
}
```
Both `only` and `exclude` should be array of package names, which can also
contain wildcards (`*`) which will match any characters.

@ -257,7 +257,7 @@ class Problem
}
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.');
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.');
}
return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.');

@ -0,0 +1,193 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Repository;
use Composer\Package\PackageInterface;
use Composer\Package\BasePackage;
/**
* Filters which packages are seen as canonical on this repo by loadPackages
*
* @author Jordi Boggiano <j.boggiano@seld.be>
*/
class FilterRepository implements RepositoryInterface
{
private $only = array();
private $exclude = array();
private $canonical = true;
private $repo;
public function __construct(RepositoryInterface $repo, array $options)
{
if (isset($options['only'])) {
if (!is_array($options['only'])) {
throw new \InvalidArgumentException('"only" key for repository '.$repo->getRepoName().' should be an array');
}
$this->only = '{^'.implode('|', array_map(function ($val) {
return BasePackage::packageNameToRegexp($val, '%s');
}, $options['only'])) .'$}iD';
}
if (isset($options['exclude'])) {
if (!is_array($options['exclude'])) {
throw new \InvalidArgumentException('"exclude" key for repository '.$repo->getRepoName().' should be an array');
}
$this->exclude = '{^'.implode('|', array_map(function ($val) {
return BasePackage::packageNameToRegexp($val, '%s');
}, $options['exclude'])) .'$}iD';
}
if ($this->exclude && $this->only) {
throw new \InvalidArgumentException('Only one of "only" and "exclude" can be specified for repository '.$repo->getRepoName());
}
if (isset($options['canonical'])) {
if (!is_bool($options['canonical'])) {
throw new \InvalidArgumentException('"canonical" key for repository '.$repo->getRepoName().' should be a boolean');
}
$this->canonical = $options['canonical'];
}
$this->repo = $repo;
}
public function getRepoName()
{
return $this->repo->getRepoName();
}
/**
* Returns the wrapped repositories
*
* @return RepositoryInterface
*/
public function getRepository()
{
return $this->repo;
}
/**
* {@inheritdoc}
*/
public function hasPackage(PackageInterface $package)
{
return $this->repo->hasPackage($package);
}
/**
* {@inheritdoc}
*/
public function findPackage($name, $constraint)
{
if (!$this->isAllowed($name)) {
return null;
}
return $this->repo->findPackage($name, $constraint);
}
/**
* {@inheritdoc}
*/
public function findPackages($name, $constraint = null)
{
if (!$this->isAllowed($name)) {
return array();
}
return $this->repo->findPackages($name, $constraint);
}
/**
* {@inheritDoc}
*/
public function loadPackages(array $packageMap, array $acceptableStabilities, array $stabilityFlags)
{
foreach ($packageMap as $name => $constraint) {
if (!$this->isAllowed($name)) {
unset($packageMap[$name]);
}
}
$result = $this->repo->loadPackages($packageMap, $acceptableStabilities, $stabilityFlags);
if (!$this->canonical) {
$result['namesFound'] = array();
}
return $result;
}
/**
* {@inheritdoc}
*/
public function search($query, $mode = 0, $type = null)
{
return $this->repo->search($query, $mode, $type);
}
/**
* {@inheritdoc}
*/
public function getPackages()
{
$result = array();
foreach ($this->repo->getPackages() as $package) {
if ($this->isAllowed($package->getName())) {
$result[] = $package;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function getProviders($packageName)
{
$result = array();
foreach ($this->repo->getProviders($packageName) as $provider) {
if ($this->isAllowed($provider['name'])) {
$result[] = $provider;
}
}
return $result;
}
/**
* {@inheritdoc}
*/
public function removePackage(PackageInterface $package)
{
return $this->repo->removePackage($package);
}
/**
* {@inheritdoc}
*/
public function count()
{
return $this->repo->count();
}
private function isAllowed($name)
{
if (!$this->only && !$this->exclude) {
return true;
}
if ($this->only) {
return (bool) preg_match($this->only, $name);
}
return !preg_match($this->exclude, $name);
}
}

@ -125,7 +125,18 @@ class RepositoryManager
$class = $this->repositoryClasses[$type];
return new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher);
if (isset($config['only']) || isset($config['exclude']) || isset($config['canonical'])) {
$filterConfig = $config;
unset($config['only'], $config['exclude'], $config['canonical']);
}
$repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher);
if (isset($filterConfig)) {
$repository = new FilterRepository($repository, $filterConfig);
}
return $repository;
}
/**

@ -28,7 +28,7 @@ Updating dependencies
Your requirements could not be resolved to an installable set of packages.
Problem 1
- Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.
- Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. See https://getcomposer.org/repoprio for details and assistance.
--EXPECT--
--EXPECT-EXIT-CODE--

@ -0,0 +1,49 @@
--TEST--
Test that filter repositories apply correctly
--COMPOSER--
{
"repositories": [
{
"type": "package",
"package": [
{ "name": "foo/a", "version": "1.0.0" }
],
"canonical": false
},
{
"type": "package",
"package": [
{ "name": "foo/a", "version": "1.0.0" },
{ "name": "foo/b", "version": "1.0.0" }
],
"only": ["foo/b"]
},
{
"type": "package",
"package": [
{ "name": "foo/a", "version": "1.2.0" },
{ "name": "foo/c", "version": "1.2.0" }
],
"exclude": ["foo/c"]
},
{
"type": "package",
"package": [
{ "name": "foo/a", "version": "1.1.0" },
{ "name": "foo/b", "version": "1.1.0" },
{ "name": "foo/c", "version": "1.1.0" }
]
}
],
"require": {
"foo/a": "1.*",
"foo/b": "1.*",
"foo/c": "1.*"
}
}
--RUN--
update
--EXPECT--
Installing foo/a (1.2.0)
Installing foo/b (1.0.0)
Installing foo/c (1.1.0)

@ -0,0 +1,69 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Repository;
use Composer\Test\TestCase;
use Composer\Repository\FilterRepository;
use Composer\Repository\ArrayRepository;
use Composer\Semver\Constraint\EmptyConstraint;
use Composer\Package\BasePackage;
class FilterRepositoryTest extends TestCase
{
private $arrayRepo;
public function setUp()
{
$this->arrayRepo = new ArrayRepository();
$this->arrayRepo->addPackage($this->getPackage('foo/aaa', '1.0.0'));
$this->arrayRepo->addPackage($this->getPackage('foo/bbb', '1.0.0'));
$this->arrayRepo->addPackage($this->getPackage('bar/xxx', '1.0.0'));
$this->arrayRepo->addPackage($this->getPackage('baz/yyy', '1.0.0'));
}
/**
* @dataProvider repoMatchingTests
*/
public function testRepoMatching($expected, $config)
{
$repo = new FilterRepository($this->arrayRepo, $config);
$packages = $repo->getPackages();
$this->assertSame($expected, array_map(function ($p) { return $p->getName(); }, $packages));
}
public static function repoMatchingTests()
{
return array(
array(array('foo/aaa', 'foo/bbb'), array('only' => array('foo/*'))),
array(array('foo/aaa', 'baz/yyy'), array('only' => array('foo/aaa', 'baz/yyy'))),
array(array('bar/xxx'), array('exclude' => array('foo/*', 'baz/yyy'))),
);
}
public function testCanonicalDefaultTrue()
{
$repo = new FilterRepository($this->arrayRepo, array());
$result = $repo->loadPackages(array('foo/aaa' => new EmptyConstraint), BasePackage::$stabilities, array());
$this->assertCount(1, $result['packages']);
$this->assertCount(1, $result['namesFound']);
}
public function testNonCanonical()
{
$repo = new FilterRepository($this->arrayRepo, array('canonical' => false));
$result = $repo->loadPackages(array('foo/aaa' => new EmptyConstraint), BasePackage::$stabilities, array());
$this->assertCount(1, $result['packages']);
$this->assertCount(0, $result['namesFound']);
}
}

@ -108,4 +108,20 @@ class RepositoryManagerTest extends TestCase
return $cases;
}
public function testFilterRepoWrapping()
{
$rm = new RepositoryManager(
$this->getMockBuilder('Composer\IO\IOInterface')->getMock(),
$config = $this->getMockBuilder('Composer\Config')->setMethods(array('get'))->getMock(),
$this->getMockBuilder('Composer\Util\HttpDownloader')->disableOriginalConstructor()->getMock(),
$this->getMockBuilder('Composer\EventDispatcher\EventDispatcher')->disableOriginalConstructor()->getMock()
);
$rm->setRepositoryClass('path', 'Composer\Repository\PathRepository');
$repo = $rm->createRepository('path', array('type' => 'path', 'url' => __DIR__, 'only' => array('foo/bar')));
$this->assertInstanceOf('Composer\Repository\FilterRepository', $repo);
$this->assertInstanceOf('Composer\Repository\PathRepository', $repo->getRepository());
}
}

Loading…
Cancel
Save