1<?php 2 3/* 4 * This file is part of Composer. 5 * 6 * (c) Nils Adermann <naderman@naderman.de> 7 * Jordi Boggiano <j.boggiano@seld.be> 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13namespace Composer\DependencyResolver; 14 15use Composer\Package\BasePackage; 16use Composer\Package\AliasPackage; 17use Composer\Package\Version\VersionParser; 18use Composer\Semver\Constraint\ConstraintInterface; 19use Composer\Semver\Constraint\Constraint; 20use Composer\Semver\Constraint\EmptyConstraint; 21use Composer\Repository\RepositoryInterface; 22use Composer\Repository\CompositeRepository; 23use Composer\Repository\ComposerRepository; 24use Composer\Repository\InstalledRepositoryInterface; 25use Composer\Repository\PlatformRepository; 26use Composer\Package\PackageInterface; 27 28/** 29 * A package pool contains repositories that provide packages. 30 * 31 * @author Nils Adermann <naderman@naderman.de> 32 * @author Jordi Boggiano <j.boggiano@seld.be> 33 */ 34class Pool implements \Countable 35{ 36 const MATCH_NAME = -1; 37 const MATCH_NONE = 0; 38 const MATCH = 1; 39 const MATCH_PROVIDE = 2; 40 const MATCH_REPLACE = 3; 41 const MATCH_FILTERED = 4; 42 43 protected $repositories = array(); 44 protected $providerRepos = array(); 45 protected $packages = array(); 46 protected $packageByName = array(); 47 protected $packageByExactName = array(); 48 protected $acceptableStabilities; 49 protected $stabilityFlags; 50 protected $versionParser; 51 protected $providerCache = array(); 52 protected $filterRequires; 53 protected $whitelist = null; 54 protected $id = 1; 55 56 public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) 57 { 58 $this->versionParser = new VersionParser; 59 $this->acceptableStabilities = array(); 60 foreach (BasePackage::$stabilities as $stability => $value) { 61 if ($value <= BasePackage::$stabilities[$minimumStability]) { 62 $this->acceptableStabilities[$stability] = $value; 63 } 64 } 65 $this->stabilityFlags = $stabilityFlags; 66 $this->filterRequires = $filterRequires; 67 foreach ($filterRequires as $name => $constraint) { 68 if (preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $name)) { 69 unset($this->filterRequires[$name]); 70 } 71 } 72 } 73 74 public function setWhitelist($whitelist) 75 { 76 $this->whitelist = $whitelist; 77 $this->providerCache = array(); 78 } 79 80 /** 81 * Adds a repository and its packages to this package pool 82 * 83 * @param RepositoryInterface $repo A package repository 84 * @param array $rootAliases 85 */ 86 public function addRepository(RepositoryInterface $repo, $rootAliases = array()) 87 { 88 if ($repo instanceof CompositeRepository) { 89 $repos = $repo->getRepositories(); 90 } else { 91 $repos = array($repo); 92 } 93 94 foreach ($repos as $repo) { 95 $this->repositories[] = $repo; 96 97 $exempt = $repo instanceof PlatformRepository || $repo instanceof InstalledRepositoryInterface; 98 99 if ($repo instanceof ComposerRepository && $repo->hasProviders()) { 100 $this->providerRepos[] = $repo; 101 $repo->setRootAliases($rootAliases); 102 $repo->resetPackageIds(); 103 } else { 104 foreach ($repo->getPackages() as $package) { 105 $names = $package->getNames(); 106 $stability = $package->getStability(); 107 if ($exempt || $this->isPackageAcceptable($names, $stability)) { 108 $package->setId($this->id++); 109 $this->packages[] = $package; 110 $this->packageByExactName[$package->getName()][$package->id] = $package; 111 112 foreach ($names as $provided) { 113 $this->packageByName[$provided][] = $package; 114 } 115 116 // handle root package aliases 117 $name = $package->getName(); 118 if (isset($rootAliases[$name][$package->getVersion()])) { 119 $alias = $rootAliases[$name][$package->getVersion()]; 120 if ($package instanceof AliasPackage) { 121 $package = $package->getAliasOf(); 122 } 123 $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); 124 $aliasPackage->setRootPackageAlias(true); 125 $aliasPackage->setId($this->id++); 126 127 $package->getRepository()->addPackage($aliasPackage); 128 $this->packages[] = $aliasPackage; 129 $this->packageByExactName[$aliasPackage->getName()][$aliasPackage->id] = $aliasPackage; 130 131 foreach ($aliasPackage->getNames() as $name) { 132 $this->packageByName[$name][] = $aliasPackage; 133 } 134 } 135 } 136 } 137 } 138 } 139 } 140 141 public function getPriority(RepositoryInterface $repo) 142 { 143 $priority = array_search($repo, $this->repositories, true); 144 145 if (false === $priority) { 146 throw new \RuntimeException("Could not determine repository priority. The repository was not registered in the pool."); 147 } 148 149 return -$priority; 150 } 151 152 /** 153 * Retrieves the package object for a given package id. 154 * 155 * @param int $id 156 * @return PackageInterface 157 */ 158 public function packageById($id) 159 { 160 return $this->packages[$id - 1]; 161 } 162 163 /** 164 * Returns how many packages have been loaded into the pool 165 */ 166 public function count() 167 { 168 return count($this->packages); 169 } 170 171 /** 172 * Searches all packages providing the given package name and match the constraint 173 * 174 * @param string $name The package name to be searched for 175 * @param ConstraintInterface $constraint A constraint that all returned 176 * packages must match or null to return all 177 * @param bool $mustMatchName Whether the name of returned packages 178 * must match the given name 179 * @param bool $bypassFilters If enabled, filterRequires and stability matching is ignored 180 * @return PackageInterface[] A set of packages 181 */ 182 public function whatProvides($name, ConstraintInterface $constraint = null, $mustMatchName = false, $bypassFilters = false) 183 { 184 if ($bypassFilters) { 185 return $this->computeWhatProvides($name, $constraint, $mustMatchName, true); 186 } 187 188 $key = ((int) $mustMatchName).$constraint; 189 if (isset($this->providerCache[$name][$key])) { 190 return $this->providerCache[$name][$key]; 191 } 192 193 return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName, $bypassFilters); 194 } 195 196 /** 197 * @see whatProvides 198 */ 199 private function computeWhatProvides($name, $constraint, $mustMatchName = false, $bypassFilters = false) 200 { 201 $candidates = array(); 202 203 foreach ($this->providerRepos as $repo) { 204 foreach ($repo->whatProvides($this, $name, $bypassFilters) as $candidate) { 205 $candidates[] = $candidate; 206 if ($candidate->id < 1) { 207 $candidate->setId($this->id++); 208 $this->packages[$this->id - 2] = $candidate; 209 } 210 } 211 } 212 213 if ($mustMatchName) { 214 $candidates = array_filter($candidates, function ($candidate) use ($name) { 215 return $candidate->getName() == $name; 216 }); 217 if (isset($this->packageByExactName[$name])) { 218 $candidates = array_merge($candidates, $this->packageByExactName[$name]); 219 } 220 } elseif (isset($this->packageByName[$name])) { 221 $candidates = array_merge($candidates, $this->packageByName[$name]); 222 } 223 224 $matches = $provideMatches = array(); 225 $nameMatch = false; 226 227 foreach ($candidates as $candidate) { 228 $aliasOfCandidate = null; 229 230 // alias packages are not white listed, make sure that the package 231 // being aliased is white listed 232 if ($candidate instanceof AliasPackage) { 233 $aliasOfCandidate = $candidate->getAliasOf(); 234 } 235 236 if ($this->whitelist !== null && !$bypassFilters && ( 237 (!($candidate instanceof AliasPackage) && !isset($this->whitelist[$candidate->id])) || 238 ($candidate instanceof AliasPackage && !isset($this->whitelist[$aliasOfCandidate->id])) 239 )) { 240 continue; 241 } 242 switch ($this->match($candidate, $name, $constraint, $bypassFilters)) { 243 case self::MATCH_NONE: 244 break; 245 246 case self::MATCH_NAME: 247 $nameMatch = true; 248 break; 249 250 case self::MATCH: 251 $nameMatch = true; 252 $matches[] = $candidate; 253 break; 254 255 case self::MATCH_PROVIDE: 256 $provideMatches[] = $candidate; 257 break; 258 259 case self::MATCH_REPLACE: 260 $matches[] = $candidate; 261 break; 262 263 case self::MATCH_FILTERED: 264 break; 265 266 default: 267 throw new \UnexpectedValueException('Unexpected match type'); 268 } 269 } 270 271 // if a package with the required name exists, we ignore providers 272 if ($nameMatch) { 273 return $matches; 274 } 275 276 return array_merge($matches, $provideMatches); 277 } 278 279 public function literalToPackage($literal) 280 { 281 $packageId = abs($literal); 282 283 return $this->packageById($packageId); 284 } 285 286 public function literalToPrettyString($literal, $installedMap) 287 { 288 $package = $this->literalToPackage($literal); 289 290 if (isset($installedMap[$package->id])) { 291 $prefix = ($literal > 0 ? 'keep' : 'remove'); 292 } else { 293 $prefix = ($literal > 0 ? 'install' : 'don\'t install'); 294 } 295 296 return $prefix.' '.$package->getPrettyString(); 297 } 298 299 public function isPackageAcceptable($name, $stability) 300 { 301 foreach ((array) $name as $n) { 302 // allow if package matches the global stability requirement and has no exception 303 if (!isset($this->stabilityFlags[$n]) && isset($this->acceptableStabilities[$stability])) { 304 return true; 305 } 306 307 // allow if package matches the package-specific stability flag 308 if (isset($this->stabilityFlags[$n]) && BasePackage::$stabilities[$stability] <= $this->stabilityFlags[$n]) { 309 return true; 310 } 311 } 312 313 return false; 314 } 315 316 /** 317 * Checks if the package matches the given constraint directly or through 318 * provided or replaced packages 319 * 320 * @param array|PackageInterface $candidate 321 * @param string $name Name of the package to be matched 322 * @param ConstraintInterface $constraint The constraint to verify 323 * @return int One of the MATCH* constants of this class or 0 if there is no match 324 */ 325 private function match($candidate, $name, ConstraintInterface $constraint = null, $bypassFilters) 326 { 327 $candidateName = $candidate->getName(); 328 $candidateVersion = $candidate->getVersion(); 329 $isDev = $candidate->getStability() === 'dev'; 330 $isAlias = $candidate instanceof AliasPackage; 331 332 if (!$bypassFilters && !$isDev && !$isAlias && isset($this->filterRequires[$name])) { 333 $requireFilter = $this->filterRequires[$name]; 334 } else { 335 $requireFilter = new EmptyConstraint; 336 } 337 338 if ($candidateName === $name) { 339 $pkgConstraint = new Constraint('==', $candidateVersion); 340 341 if ($constraint === null || $constraint->matches($pkgConstraint)) { 342 return $requireFilter->matches($pkgConstraint) ? self::MATCH : self::MATCH_FILTERED; 343 } 344 345 return self::MATCH_NAME; 346 } 347 348 $provides = $candidate->getProvides(); 349 $replaces = $candidate->getReplaces(); 350 351 // aliases create multiple replaces/provides for one target so they can not use the shortcut below 352 if (isset($replaces[0]) || isset($provides[0])) { 353 foreach ($provides as $link) { 354 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { 355 return $requireFilter->matches($link->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; 356 } 357 } 358 359 foreach ($replaces as $link) { 360 if ($link->getTarget() === $name && ($constraint === null || $constraint->matches($link->getConstraint()))) { 361 return $requireFilter->matches($link->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; 362 } 363 } 364 365 return self::MATCH_NONE; 366 } 367 368 if (isset($provides[$name]) && ($constraint === null || $constraint->matches($provides[$name]->getConstraint()))) { 369 return $requireFilter->matches($provides[$name]->getConstraint()) ? self::MATCH_PROVIDE : self::MATCH_FILTERED; 370 } 371 372 if (isset($replaces[$name]) && ($constraint === null || $constraint->matches($replaces[$name]->getConstraint()))) { 373 return $requireFilter->matches($replaces[$name]->getConstraint()) ? self::MATCH_REPLACE : self::MATCH_FILTERED; 374 } 375 376 return self::MATCH_NONE; 377 } 378} 379